Generating static charts

Some dashboards require charts in them to be fully interactive. Some don't. Sometimes we wish to churn out charts to the screen as as quick as possible, and with as little memory footprint as possible. This tutorial will walk through the process of generating static SVG for charts using a single chart template.

Dynamic vs. Static

When we instantiate a chart it takes a chunk out of memory, as well as CPU cycles. It also does all kinds of other stuff - measuring, adding event handlers, browser capability probing, etc.

This is done, because chart is a "live" element. It can react to user input, pointers, live code, and whatnot.

If we have a single chart, it's probably not an issue.

But, with bigger dashboards, it might just become one, with each new chart putting additional strain on memory and CPU.

Say we need to display 20 sparkline charts.

There are two ways to go about it:

  1. Create a separate chart object for each sparkline.
  2. Create a single chart instance, then update data/settings for it and generate a static SVG representation with each iteration.

If we need our charts to be interactive or otherwise modifiable, we'll stuck with option #1.

However, if we don't care about all of that, the option #2 will require significantly less resources, and might be a better choice for us. This tutorial will focus on implementing it. Read on.

Building static charts

Conceptually this works like that:

  1. Create a chart instance we will be using as a "template".
  2. Update chart's data and/or settings as needed.
  3. Grab resulting SVG the chart generates.
  4. Put SVG as a separate element somewhere else.
  5. Go back to step #2 for each subsequent chart.
  6. Destroy "template" chart.

This way we get a bunch of charts without the overhead of having a lot of resource-heavy chart objects.

Let's look at each of those steps in detail.

Template chart

Creating template chart

We're going to assume you already know how to create a conventional interactive chart. For the sake of this tutorial we're going to be creating sparkline charts.

SIDE READING For details on creating sparklines, make sure you check out our "Creating sparklines and microcharts" tutorial.

Here's a sample code:

am4core.useTheme(am4themes_microchart);

let chart = am4core.create("chartdiv", am4charts.XYChart);
chart.maskBullets = false;

let dateAxis = chart.xAxes.push(new am4charts.DateAxis());
let valueAxis = chart.yAxes.push(new am4charts.ValueAxis());

let series = chart.series.push(new am4charts.LineSeries());
series.tooltipText = "{date}: [bold]{value}";
series.dataFields.dateX = "date";
series.dataFields.valueY = "value";

// Add a bullet for the last data point
let bullet = series.bullets.push(new am4charts.CircleBullet());
bullet.circle.opacity = 1;
bullet.disabled = true;
bullet.propertyFields.disabled = "disabled";
bullet.circle.radius = 3;
am4core.useTheme(am4themes_microchart);

var chart = am4core.create("chartdiv", am4charts.XYChart);
chart.maskBullets = false;

var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());

var series = chart.series.push(new am4charts.LineSeries());
series.tooltipText = "{date}: [bold]{value}";
series.dataFields.dateX = "date";
series.dataFields.valueY = "value";

// Add a bullet for the last data point
var bullet = series.bullets.push(new am4charts.CircleBullet());
bullet.circle.opacity = 1;
bullet.disabled = true;
bullet.propertyFields.disabled = "disabled";
bullet.circle.radius = 3;
{
  // Generic settings
  "maskBullets": false,

  // Axes
  "xAxes": [{
    "type": "DateAxis"
  }],

  "yAxes": [{
    "type": "ValueAxis"
  }],

  // Series
  "series": [{
    "type": "LineSeries",
    "dataFields": {
      "dateX": "date",
      "valueY": "value"
    },
    "bullets": [{
      "type": "CircleBullet",
      "disabled": true,
      "circle": {
        "opacity": 1,
        "radius": 3
      },
      "propertyFields": {
        "disabled": "disabled"
      }
    }]
  }]
}

NOTE The above code assumes we are using microchart theme, which eliminates the need to disable certain elements like grid, labels, margins, making the chart suitable for very small spaces.

Hiding template chart

Since we are going to be using the template chart only as a "cookie cutter", we probably don't want it to be appearing on screen.

Simply hiding it using CSS display: none will not do because hidden elements do not have any dimensions and that will mess up chart's internal measurements resulting in broken layouts.

The best way to hide it without breaking its measurements is to position it globally and off-screen, e.g.:

#chartdiv {
  width: 150px;
  height: 60px;
  position: fixed;
  top: -1000px;
}

Getting static SVG snapshot

Now that we have our template chart, we need a function that would grab chart's snapshot in form of a static SVG markup.

We need it as a function, because we're going to be using it quite heavily - we're going to be stamping out a lot of static charts.

Since amCharts 4 comes with built-in exporting functionality, we can use a few of its functions to serialize chart (or just parts of it as we'll see later) as a static SVG. Here's an example:

let target = chart.chartAndLegendContainer;
let svg = chart.exporting.normalizeSVG(
  chart.exporting.serializeElement(target.dom),
  {},
  target.pixelWidth,
  target.pixelHeight
);
var target = chart.chartAndLegendContainer;
var svg = chart.exporting.normalizeSVG(
  chart.exporting.serializeElement(target.dom),
  {},
  target.pixelWidth,
  target.pixelHeight
);

The two export functions are used in the above code:

  • serializeElement: it takes an actual DOM element as parameter and returns its SVG representation. We're passing in chartAndLegendContainer here, but you could pass the whole chart (chart.dom), or any other element, e.g. plot area (chart.plotContainer.dom).
  • The above funciton returns SVG markup relevant to the target not - no SVG headers, and other necessary stuff. That's why we pass it through normalizeSVG() function, which not only adds relevant headers, wraps into required tags, but also sets the size of the SVG to the same as the chart itself.

Now hat we have a full-fledged SVG of the chart (or rather chart's element), we can do what we want with it, e.g. place it on the web page:

document.getElementById("sparklines").innerHTML += svg;
document.getElementById("sparklines").innerHTML += svg;

We'll hit you up with a functional demo a bit later in this tutorial.

Generating multiple charts

We've already established that the whole point of this exercise is to generate multiple static charts out of a single chart template.

Just to refresh, it works by refreshing chart data, grabbing SVG snapshot, placing it somewhere, then updating data again for the next snapshot, and so on.

Updating chart's or series' data is as easy as setting its data property to something new.

However it's not as easy, because charts being asynchronous creatures are not immediately ready for export. We have to wait until they're ready.

The most safe setup here is to constantly check if chart still has any invalid element (that haven't been drawn yet), then making a grab of the SVG. Here's how it works:

function processNextChart() {
  let newData = getNewData();
  if (newData) {
    series.data = newData;
    series.events.once("dataitemsvalidated", function() {
      createImage();
    });
  }
  else {
    chart.dispose();
  }
}

function createImage() {
  let invalidSprites = am4core.registry.invalidSprites[chart.uid];
  if (invalidSprites && invalidSprites.length) {
    am4core.registry.events.once("exitframe", function() {
      createImage();
    });
  }
  else {
    am4core.registry.events.once("exitframe", function() {
      let target = chart.chartAndLegendContainer;
      let svg = chart.exporting.normalizeSVG(
        chart.exporting.serializeElement(target.dom),
        {},
        target.pixelWidth,
        target.pixelHeight
      );
      let div = document.getElementById("sparklines");
      div.innerHTML += svg;
      processNextChart();
    });
  }
}
function processNextChart() {
  var newData = getNewData();
  if (newData) {
    series.data = newData;
    series.events.once("dataitemsvalidated", function() {
      createImage();
    });
  }
  else {
    chart.dispose();
  }
}

function createImage() {
  var invalidSprites = am4core.registry.invalidSprites[chart.uid];
  if (invalidSprites && invalidSprites.length) {
    am4core.registry.events.once("exitframe", function() {
      createImage();
    });
  }
  else {
    am4core.registry.events.once("exitframe", function() {
      var target = chart.chartAndLegendContainer;
      var svg = chart.exporting.normalizeSVG(
        chart.exporting.serializeElement(target.dom),
        {},
        target.pixelWidth,
        target.pixelHeight
      );
      var div = document.getElementById("sparklines");
      div.innerHTML += svg;
      processNextChart();
    });
  }
}

Notice how processNextChart() function sets up a once-off event handler to kick in when series' data items are validated (new data is taken in) to kick another function: createImage().

createImage() in its turn checks am4core.registry.invalidSprites if there are any invalid sprites for our chart. If there are, it means the chart is still building itself after the last data update and we should try in the next frame, and so on, until there are no invalid sprites - the chart is ripe and ready, so we can grab its SVG snapshot.

Cleaning up

Once we're done generating static charts, it's time to clean up.

This means that template chart, which served as a cookie cutter, is no longer needed.

We can free up even more resources by destroying it:

chart.dispose();
chart.dispose();

Example

See the Pen amCharts 4: Generating sparklines tutorial (1) by amCharts team (@amcharts) on CodePen.24419