Modifying chart for export

Version 4.6.8 brought a new feature to chart exporting functionality: ability to modify chart appearance any way we want before export. For example we'd like to watermark exported charts with an attribution message. Or we want to add any additional titles or info. This tutorial will show how we can do it.

The task

Let's say we have a chart which we let our users to download as image or PDF. They can do so using export menu.

When export functionality is triggered, a snapshot of the chart on screen is generated and offered as a download to user.

When the chart is displayed as part of the website, any viewer checking it knows it's our data and data viz.

However, when chart is exported to a standalone image, it starts its life on its own. We don't have any control where on how it is posted or displayed.

So, we want to slap a little watermark on it, so that no matter where life takes our chart, people viewing it will see where it came from.

The solution

Adding watermark

Without making it overly complicated, let's add a small copyright label directly to our chart.

let watermark = chart.createChild(am4core.Label);
watermark.text = "Source: [bold]amcharts.com[/]";
watermark.align = "right";
watermark.fillOpacity = 0.5;
var watermark = chart.createChild(am4core.Label);
watermark.text = "Source: [bold]amcharts.com[/]";
watermark.align = "right";
watermark.fillOpacity = 0.5;
{
  // ...
  "children": [{
    "type": "Label",
    "forceCreate": true,
    "text": "Source: [bold]amcharts.com[/]",
    "align": "right",
    "fillOpacity": 0.5
  }]
}

Now, if we run our chart, we get a small copyright label on the bottom right.

All good, but we don't need it cluttering our screen. We just want it to be visible when chart is exported to image.

So, let's disable it for now:

watermark.disabled = true;
watermark.disabled = true;
{
  // ...
  "children": [{
    "type": "Label",
    "forceCreate": true,
    "text": "Source: [bold]amcharts.com[/]",
    "align": "right",
    "fillOpacity": 0.5,
    "disabled": true
  }]
}

Enabling watermark on export

Now, watermark gone from screen, creates another problem: export creates an exact copy of the screen chart, which means no watermark.

We need to enable it when export starts, then disable it again when it finishes.

For that, we have two suitable events: exportstarted and exportfinished.

Let's use them:

// Enable watermark on export
chart.exporting.events.on("exportstarted", function(ev) {
  watermark.disabled = false;
});

// Disable watermark when export finishes
chart.exporting.events.on("exportfinished", function(ev) {
  watermark.disabled = true;
});
// Enable watermark on export
chart.exporting.events.on("exportstarted", function(ev) {
  watermark.disabled = false;
});

// Disable watermark when export finishes
chart.exporting.events.on("exportfinished", function(ev) {
  watermark.disabled = true;
});
{
  // ...
  "exporting": {
    // ...
    "events": {

      // Enable watermark on export
      "exportstarted": function(ev) {
        var chart = ev.target.sprite;
        var watermark = chart.children.last();
        watermark.disabled = false;
      },

      // Disable watermark when export finishes
      "exportfinished": function(ev) {
         var chart = ev.target.sprite;
         var watermark = chart.children.last();
         watermark.disabled = true;
       }
 
   }
  }
}

The code above should achieve what we want, except it might not because there's a catch. Read on.

Validating updates

All chart element updates in amCharts 4 are fully asynchronous.

This means that when "exportstarted" event kicks in it does not stop there to wait until any updates made to chart elements are actually take effect, but rather proceed to actual export.

This is why, depending on a lot of factors, the watermark might not re-enable itself in time export snaps a picture of the chart.

This is where export's setting validateSprites comes in.

It's an array where we can push any elements that absolutely need to be fully validated and ready before the export operation can commence.

When you update an element (i.e. set its disabled property) it immediately becomes invalid, signaling rendering engine that it needs to be repainted.

By inserting the element into validateSprites array, we're effectively telling export to wait for it to become valid (repainted) before proceeding.

Let's try that:

chart.exporting.validateSprites.push(watermark);
chart.exporting.validateSprites.push(watermark);
{
  // ...
  "exporting": {
    // ...
    "callback": function() {
      var chart = this.sprite;
      var watermark = chart.children.last(); 
      this.validateSprites.push(watermark);
    } 
  }
}

Examples

Watermark

Below is a live example of the chart we were working with during this tutorial.

See the Pen qBBEdWK by amCharts team (@amcharts) on CodePen.24419

Dynamic info

The below demo has a chart title element, which is disabled at first.

When exportstarted event kicks in, it checks current zoom range, and attaches that info to title, then enables it, so when image is exported, it clearly shows the range of the selection in it.

See the Pen amCharts 4: Including dynamic information into export by amCharts team (@amcharts) on CodePen.24419

Gotcha

Like with most things that sound too good, there is a catch.

Some elements when changed may change its only appearance only (i.e. changing color of a label). For those cases it's safe to use techniques above.

In other cases, changing an element might influence layout of nearby elements, or even the whole chart.

In such cases, not only the element itself is invalided but also its direct parent because layout inside changes, and in turn its own parent.

We need to take this into account when specifying what elements need to be waited for to validate.

Furthermore, when element is changed and invalidated, it might not invalidate its parent element immediately, so export will not wait for it to validate (it's already valid when exportstarted kicks in). We will need to invalidate them manually.

Below is an example which enables chart title on export. Since title wasn't there before, it starts occupying more space, leaving less of it for the plot area, meaning the whole series will need to be repainted.

This is why we not only add watch for the title element, but also for its parent AND invalidate the parent manually:

// Add title
let title = chart.titles.create();
title.text = "Revenue";
title.disabled = true;

// Enable title on export
chart.exporting.events.on("exportstarted", function(ev) {
  title.disabled = false;
  title.parent.invalidate();
});

// Disable title when export finishes
chart.exporting.events.on("exportfinished", function(ev) {
  title.disabled = true;
});

// Add title to validated sprites
chart.exporting.validateSprites.push(title);
chart.exporting.validateSprites.push(title.parent);
// Add title
var title = chart.titles.create();
title.text = "Revenue";
title.disabled = true;

// Enable title on export
chart.exporting.events.on("exportstarted", function(ev) {
  title.disabled = false;
  title.parent.invalidate();
});

// Disable title when export finishes
chart.exporting.events.on("exportfinished", function(ev) {
  title.disabled = true;
});

// Add title to validated sprites
chart.exporting.validateSprites.push(title);
chart.exporting.validateSprites.push(title.parent);
{
  // ...
  "titles": [{
    "text": "Revenue",
    "disabled": true
  }],
  "exporting": {
    // ...
    "events": {

      // Enable watermark on export
      "exportstarted": function(ev) {
        var title = ev.target.sprite.titles.getIndex(0);
        title.disabled = false;
        title.parent.invalidate();
      },

      // Disable watermark when export finishes
      "exportfinished": function(ev) {
         var title = ev.target.sprite.titles.getIndex(0);
         title.disabled = true;
       }
 
   }
  }
}