Featured image of post Creating an Apache ECharts Hugo shortcode

Creating an Apache ECharts Hugo shortcode

This is mostly documentation for myself summarizing what I did to add a echarts shortcode to Hugo that allows me to support Apache Echarts on this blog. I initially thought I would use this to add a Treemap visualization of Todd Conklin’s Five Principles of Human Performance. These steps are very similar to Navendu Pottekkat’s Adding Diagrams to Your Hugo Blog With Mermaid.

Some of these steps are depend on the hugo-theme-stack partials. They will likely need to be changed to support other themes.

Steps

Include the ECharts source

Download the ECharts source from the download page. Expand the archive and ensure the contents exist in the static/echarts directory.

Configure the theme params

Configure the light and dark echart themes via the site’s config.yaml. The partial renderers will use these basenames when resolving theme paths in the static/echarts/theme directory.

params:
  echarts:
    theme:
      light: infographic
      dark: dark-digerati

Header Partial

Upsert the layouts/partials/head/custom.html partial so that it conditionally includes the ECharts partial render. Posts will declare whether they need EChart support by frontmatter. This eliminates downloading unnecessary files for the majority of the pages.

<!-- Add echarts js file -->
{{ if (.Params.echarts) }}
{{ partial "echarts.html" }}
{{ end }}

The layouts/partials/head/custom.html conditionally includes the EChart specific partial layouts/partials/echarts.html. The JavaScript in the echarts shortcode will check the Hugo theme’s local storage value when rendering to determine which theme to use.

<!-- Include the ECharts file you just downloaded -->
<script src="/echarts/dist/echarts.js"></script>

<!-- Include the theme files -->
<script src="/echarts/theme/{{ if site.Params.echarts.theme.light }}{{ site.Params.echarts.theme.light }}{{ else }}default{{ end }}.js"></script>
<script src="/echarts/theme/{{ if site.Params.echarts.theme.dark }}{{ site.Params.echarts.theme.dark }}{{ else }}dark{{ end }}.js"></script>

This source optionally dereferences the theme names defined in the site’s primary config.yaml. There’s also the ECharts Theme Builder to create a completely custom design.

Usage

The first requirement is to declare an EChart dependency in the post’s frontmatter:

echarts: true

To build a graph, the shortcode requires user data to create the DOM elements and initialize the chart:

  • Required
    • ElementID
    • Chart Definition (either inline or as an external JS file)
  • Optional
    • Dimensions (width and height optional)
    • Interactivity

Required

The shortcode requires an element ID to use for the DOM. This enables multiple charts per page:

{{< echarts id="contentInline" >}}

Chart content can be provided either inline or externally by a JavaScript file that defines an immediately executed function. The options returned by either source are provided to the setOption call.

For inline data:

{{< echarts id="contentInline" >}}
{
  title: {
      text: "Inline Chart Definition",
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'line'
    }
  ]
}
{{< /echarts >}}

Renders as (ignoring the optional width and height params):

I started with this approach, but missed VSCode’s syntax highlighting. An alternative version is to define an immediate JS function in an external file.

{{< echarts id="externalContent" srcChart="content/posts/2024/10/EChart Shortcode/line.js" >}}
{{< /echarts >}}

Renders as:

You can view the full line.js source here.

Optional - Dimensions

The shortcode supports optional width (default=1024px) and height (default=800px) params:

{{< echarts id="externalContent" width="800px" height="600px" >}}
{{< /echarts >}}

Optional - Interactivity

Some ECharts like the gauge require event handlers to be configured. For these types of charts the entire configuration can be externalized into a jsSource param:

{{< echarts id="jsInteractive" width="400px" height="400px" jsSource="./gauge.js" >}}
{{< /echarts >}}

The content of the jsSource file is an immediate JavaScript function like:

(function() {
    var chartOps = {
        ...
    };
    return chartOps;
})

View the full source here to see more. The content of this file renders as:

Shortcode

The shortcode itself is responsible for creating a unique DOM element, constructing the chart input from either the inline or external source, and then optionally loading the interactive data.


{{ $idName := .Get "id"}}
{{ $safeJSIDalue := $idName | safeJS}}
<div id="{{ $idName }}" 
  style="width: {{ if .Get "width" }}{{ .Get "width" }}{{ else }}1024px{{ end }}; height: {{ if .Get "height" }}{{ .Get "height" }}{{ else }}800px{{ end }};"></div>

<script type="text/javascript"> 
    // Determine which theme to use. Reference:
    // https://github.com/CaiJimmy/hugo-theme-stack/blob/839fbd0ecb5bba381f721f31f5195fb6517fc260/layouts/partials/head/colorScheme.html#L24
    var echartTheme = '{{ if site.Params.echarts.theme.light }}{{ site.Params.echarts.theme.light }}{{ else }}default{{ end }}';
    if (document.documentElement.dataset.scheme == 'dark') {
      echartTheme = '{{ if site.Params.echarts.theme.dark }}{{ site.Params.echarts.theme.dark }}{{ else }}dark{{ end }}';
    }
    // Initialize the echarts instance based on the prepared dom
    var myChart = echarts.init(document.getElementById('{{ $idName }}'), echartTheme);

    // Specify the configuration items and data for the chart
    var defaultOptions = {
      title: {
        text: ''
      },
    };
    var userOptions = {};
    try {
      // Load the data either from an external file or inline
      {{ if isset .Params "srcChart" }}
        {{- $srcPath := .Get "srcChart" -}}
        {{- $chartSourceData := readFile $srcPath -}}
        console.log("Using external script data");
        const userChartOptions = eval({{ $chartSourceData }});
        userOptions = userChartOptions;
      {{ else }}
        console.log("Using .Inner data");
        userOptions = eval("(" + {{.Inner}} + ")");
      {{ end }}
    }
    catch (err) {
      console.log("Failed to parse echart data: " + err)
    }
    // Display the chart using the configuration items and data just specified.
    const chartOptions_{{ $safeJSIDalue }} = { ...defaultOptions, ...userOptions};
    myChart.setOption(chartOptions_{{ $safeJSIDalue }});
</script>
{{ if isset .Params "jsSource"}}<script src={{ .Get "jsSource" }} ></script>{{ end }}

Attributions

Photo by Pankaj Patel on Unsplash