Generating static chart snapshots

This tutorial will introduce to a concept of generating static snapshots of the charts.

The concept

In some cases, we do not need chart interactivity.

For example, if we want to show a grid with multiple sparklines/microcharts, we might need only the visual representation.

In such a case it would be waste of resources - memory and CPU - to keep multiple interactive charts on the page.

This is where generating static snapshots comes in handy.

The process involves:

  • Creating a temporary <div> element.
  • Creating a live chart in it.
  • Getting a snapshot of the chart as a static image or canvas.
  • Disposing the live chart as well as the temporary <div> element.
  • Placing the static chart image where we want it to be on the page.

Prerequisites

Since chart creation as well as its export are asynchronous we will be using Promises.

We will also be using amCharts 5 plugin Exporting for generating static snapshot of the chart.

Snapshot function

Defining snapshot function

The following function takes these parameters:

  • width and height - dimensions for the temporary <div> element, that the chart will be created in.
  • chartFunc - a function that actually creates the chart. It should accept a reference to a <div> element that it should create itself in, as well as data to be used for the chart. It should return a Root element it creates. Modify this for your needs.
  • data - chart data, to be passed on to chartFunc.

The chartFunc returns a Promise, which when resolves receives a full-fledged <canvas> element.

function getChartCanvas(width, height, chartFunc, data) {
  
  // Create promise
  const promise = new Promise(function(resolve, reject) {
    // Create temporary container for chart
    let div = document.createElement("div");
    div.style.width = width + "px";
    div.style.height = height + "px";
    div.style.position = "absolute";
    div.style.top = "-1000px";
    document.body.appendChild(div);

    // Generate chart
    const root = chartFunc.call(this, div, data);

    let timeout;
    root.events.on("frameended", function() {
      if (timeout) {
        clearTimeout(timeout);
      }
      timeout = setTimeout(function() {
        // Export chart
        const exporting = am5plugins_exporting.Exporting.new(root, {
          backgroundOpacity: 0
        });
        exporting.getCanvas({}).then(function(canvas) {
          canvas.style.position = "relative";
          canvas.style.top = "";
          canvas.style.width = width + "px";
          canvas.style.height = height + "px";

          // Remove container and chart
          root.dispose();
          document.body.removeChild(div);
          
          resolve(canvas);
        });
      }, 100);
    });
  
  });
  
  return promise;
}
function getChartCanvas(width, height, chartFunc, data) {
  
  // Create promise
  const promise = new Promise(function(resolve, reject) {
    // Create temporary container for chart
    var div = document.createElement("div");
    div.style.width = width + "px";
    div.style.height = height + "px";
    div.style.position = "absolute";
    div.style.top = "-1000px";
    document.body.appendChild(div);

    // Generate chart
    var root = chartFunc.call(this, div, data);

    var timeout;
    root.events.on("frameended", function() {
      if (timeout) {
        clearTimeout(timeout);
      }
      timeout = setTimeout(function() {
        // Export chart
        var exporting = am5plugins_exporting.Exporting.new(root, {
          backgroundOpacity: 0
        });
        exporting.getCanvas({}).then(function(canvas) {
          canvas.style.position = "relative";
          canvas.style.top = "";
          canvas.style.width = width + "px";
          canvas.style.height = height + "px";

          // Remove container and chart
          root.dispose();
          document.body.removeChild(div);
          
          resolve(canvas);
        });
      }, 100);
    });
  
  });
  
  return promise;
}

Most of the code above is self-explanatory, but the "frameended" part is worth exploring further.

As we mentioned before, chart creation is an asynchronous process. This is why we can't start exporting it as soon as chart is created, lest we get a blank canvas.

To fix that, we are debouncing our export code 100ms until "frameended" events stop kicking in - at which point we know that the chart has finished building itself completely.

PNG or Canvas

If we'd prefer a PNG image instead of canvas, we can modify the exporting function to return image instead:

function getChartImage(width, height, chartFunc, data) {
  
  // Create promise
  const promise = new Promise(function(resolve, reject) {
    // Create temporary container for chart
    const div = document.createElement("div");
    div.style.width = width + "px";
    div.style.height = height + "px";
    div.style.position = "absolute";
    div.style.top = "-1000px";
    document.body.appendChild(div);

    // Generate chart
    const root = chartFunc.call(this, div, data);

    let timeout;
    root.events.on("frameended", function() {
      if (timeout) {
        clearTimeout(timeout);
      }
      timeout = setTimeout(function() {
        // Export chart
        const exporting = am5plugins_exporting.Exporting.new(root, {
          backgroundOpacity: 0
        });
        exporting.exportImage("png", {}).then(function(datauri) {
          var img = document.createElement("img");
          img.src = datauri;
          img.style.position = "relative";
          img.style.top = "";
          img.style.width = width + "px";
          img.style.height = height + "px";

          // Remove container and chart
          root.dispose();
          document.body.removeChild(div);
          
          resolve(img);
        });
      }, 100);
    });
  
  });
  
  return promise;
}
function getChartImage(width, height, chartFunc, data) {
  
  // Create promise
  var promise = new Promise(function(resolve, reject) {
    // Create temporary container for chart
    var div = document.createElement("div");
    div.style.width = width + "px";
    div.style.height = height + "px";
    div.style.position = "absolute";
    div.style.top = "-1000px";
    document.body.appendChild(div);

    // Generate chart
    var root = chartFunc.call(this, div, data);

    var timeout;
    root.events.on("frameended", function() {
      if (timeout) {
        clearTimeout(timeout);
      }
      timeout = setTimeout(function() {
        // Export chart
        var exporting = am5plugins_exporting.Exporting.new(root, {
          backgroundOpacity: 0
        });
        exporting.exportImage("png", {}).then(function(datauri) {
          var img = document.createElement("img");
          img.src = datauri;
          img.style.position = "relative";
          img.style.top = "";
          img.style.width = width + "px";
          img.style.height = height + "px";

          // Remove container and chart
          root.dispose();
          document.body.removeChild(div);
          
          resolve(img);
        });
      }, 100);
    });
  
  });
  
  return promise;
}

Chart creation function

Here's how simple function that creates a live chart might look like:

function createVolumeChart(div, data) {
  const root = am5.Root.new(div);

  root.setThemes([
    am5themes_Micro.new(root)
  ]);

  const chart = root.container.children.push(am5xy.XYChart.new(root, {
    panX: false,
    panY: false,
    wheelX: "none",
    wheelY: "none"
  }));
  
  chart.plotContainer.set("wheelable", false);
  chart.zoomOutButton.set("forceHidden", true);

  const xAxis = chart.xAxes.push(am5xy.DateAxis.new(root, {
    maxDeviation: 0,
    baseInterval: { timeUnit: "day", count: 1 },
    renderer: am5xy.AxisRendererX.new(root, {})
  }));

  const yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {})
  }));


  const series = chart.series.push(am5xy.ColumnSeries.new(root, {
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "volume",
    valueXField: "date",
    fill: am5.color(0x999999)
  }));
  
  series.data.setAll(data);
  return root;
}
function createVolumeChart(div, data) {
  var root = am5.Root.new(div);

  root.setThemes([
    am5themes_Micro.new(root)
  ]);

  var chart = root.container.children.push(am5xy.XYChart.new(root, {
    panX: false,
    panY: false,
    wheelX: "none",
    wheelY: "none"
  }));
  
  chart.plotContainer.set("wheelable", false);
  chart.zoomOutButton.set("forceHidden", true);

  var xAxis = chart.xAxes.push(am5xy.DateAxis.new(root, {
    maxDeviation: 0,
    baseInterval: { timeUnit: "day", count: 1 },
    renderer: am5xy.AxisRendererX.new(root, {})
  }));

  var yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, {
    renderer: am5xy.AxisRendererY.new(root, {})
  }));


  var series = chart.series.push(am5xy.ColumnSeries.new(root, {
    xAxis: xAxis,
    yAxis: yAxis,
    valueYField: "volume",
    valueXField: "date",
    fill: am5.color(0x999999)
  }));
  
  series.data.setAll(data);
  return root;
}

NOTEPlease note how we are using a "Micro" theme in the code above. It's ideal for creating sparklines as it automatically removes all extra elements like labels and grid for us.

Usage

Here's how we may use the snapshot function:

getChartCanvas(100, 50, createVolumeChart, data).then(function(canvas) {
  document.body.appendChild(canvas);
});
getChartCanvas(100, 50, createVolumeChart, data).then(function(canvas) {
  document.body.appendChild(canvas);
});

Demo

The following demo uses above functions to create a grid of stock tickers with static sparklines.

See the Pen
Generating static canvas sparkline charts
by amCharts team (@amcharts)
on CodePen.0

And here's the same demo exporting PNGs instead:

See the Pen
Generating static canvas sparkline charts (PNG)
by amCharts team (@amcharts)
on CodePen.0

Posted in Uncategorized