Automating report generation using Puppeteer

If you need to automate the generation of chart images or PDF, utilities with a self contained browser are your best options. There are few options out there as headless browsers.

We created this example based on Puppeteer as this is a new option supported by Google that uses latest Chrome.

What is Puppeteer

Puppetter is a npm package that contains Chrome on its distribution. When you run Puppeteer,
Chrome opens and you can run a given page in it.

Check more about Puppeteer on the links below:

Setting up

  • Install NodeJS
  • Create a folder
  • Create a package.json file with the content shown below:
    {
      "name": "amCharts 4",
      "description": "Automate chart export",
      "engines": {
        "node": ">=6.0.0"
      },
      "dependencies": {
        "puppeteer": "^1.5.0"
      }
    }
    
  • Open a terminal and run npm i on the same folder to install the dependencies

Generating a single chart report from external link

You can export charts that exist on a current page.

Add a file called external.js for now. You can rename it as you wish. Add the code below
in it:

const puppeteer = require('puppeteer');

(async () => {

  let url = 'https://s.codepen.io/team/amcharts/fullpage/eKoVEP';
  
  // Open the browser
  const browser = await puppeteer.launch({
    // Run the browser headless or not
    headless: true
  });

  // Open a tab
  const page = await browser.newPage();

  // Set the referer. That is only needed if the server needs it for security reasons
  await page.setExtraHTTPHeaders({
    'referer': url
  });
  await page.setRequestInterception(true);
  page.on('request', request => {
    request.continue();
  });

  // Defines where the file should be saved
  await page._client.send('Page.setDownloadBehavior', {
    behavior: 'allow',
    // Using a folder at the root to save the images
    downloadPath: '/export'
  });

  // Define the URL where the chart is
  await page.goto(url);

  // Run scripts in the page
  await page.evaluate(() => {

    // Make the evaluate callback wait until the export is done
    return new Promise((resolve, reject) => {
    
      // Call the export on the chart
      chart.exporting.export('pdf').finally(() => {

        // Wait for the download to get done
        setTimeout(() => {
          resolve();
        }, 2000);
      });
    });
  });

  // Close the browser
  await browser.close();
})();

Now run node external.js.

NOTE The exported files will be in the export folder under the root folder of your computer.

Generating a single chart report from the same package

If you prefer to create charts without requirement external pre-existing pages, you can have them built into this solution. We will setup a server using HapiJS by first adding the following dependencies in the package.json file:

{
  "name": "amCharts 4",
  "description": "Automate chart export",
  "engines": {
    "node": ">=6.0.0"
  },
  "dependencies": {
    "puppeteer": "^1.5.0",
    "hapi": "17.5.1",
    "inert": "5.1.0"
  }
}

Run npm i to install the new dependencies.

Add server.js file and the following code into it:

const Hapi = require('hapi');
const Inert = require('inert');
const exportChart = require('./internal');

const server = new Hapi.Server({
  host: 'localhost',
  port: 3000
});

server.route({
  method: 'GET',
  path: '/',
  handler: (request, h) => {
    return h.file('./public/index.html');
  }
});

server.route({
  method: 'GET',
  path: '/{fileName*}',
  handler: (request, h) => {
    return h.file('./public/' + request.params.fileName);
  }
});

const init = async () => {

  await server.register(Inert);
  await server.start();

  console.log(`Server running at: ${server.info.uri}`);
}

server.events.on('start', () => {

  // Start the export feature
  exportChart(server);

  console.log('Server started and the export module is loaded');
});

server.events.on('stop', () => {
  console.log('Server stopped');
});

init();

Adding files and folders

At this point you should some of the files below. Please add the missing folders and files as shown and then the correspondent content:

/
├── package.json
├── server.js
├── internal.js
├── external.js
└── public/
  ├── index.html
  ├── styles/
  |   └── app.css
  └── scripts/
    └── chart.js

The index.html file

<!DOCTYPE html>
<html lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>amCharts 4</title>

  <link rel="stylesheet" href="/styles/app.css">

</head>
<body>

  <div id="chartdiv"></div>

  <script src="//www.amcharts.com/lib/4/core.js"></script>
  <script src="//www.amcharts.com/lib/4/charts.js"></script>
  <script src="//www.amcharts.com/lib/4/themes/animated.js"></script>

  <script src="/scripts/chart.js"></script>

</body>
</html>

The app.css file

@import url("https://fonts.googleapis.com/css?family=Archivo+Narrow");

body {
  font-family: "Archivo Narrow", Arial;
}

#chartdiv {
  width: 100%;
  height: 500px;
}

The chart.js file

am4core.useTheme(am4themes_animated);

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

chart.data = [{
  category: "One",
  value1: 1,
  value2: 5,
  value3: 3,
  value4: 3
}, {
  category: "Two",
  value1: 2,
  value2: 5,
  value3: 3,
  value4: 4
}, {
  category: "Three",
  value1: 3,
  value2: 5,
  value3: 4,
  value4: 4
}, {
  category: "Four",
  value1: 4,
  value2: 5,
  value3: 6,
  value4: 4
}, {
  category: "Five",
  value1: 3,
  value2: 5,
  value3: 4,
  value4: 4
}, {
  category: "Six",
  value1: 8,
  value2: 7,
  value3: 10,
  value4: 4
}, {
  category: "Seven",
  value1: 10,
  value2: 8,
  value3: 6,
  value4: 4
}];

chart.legend = new am4charts.Legend();
chart.colors.step = 2;

var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
categoryAxis.dataFields.category = "category";
categoryAxis.renderer.grid.template.location = 0;

var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.min = 0;
valueAxis.renderer.minWidth = 35;

var series1 = chart.series.push(new am4charts.ColumnSeries());
series1.columns.template.width = am4core.percent(80);
series1.columns.template.tooltipText = "{name}: {valueY.value}";
series1.name = "Series 1";
series1.dataFields.categoryX = "category";
series1.dataFields.valueY = "value1";
series1.stacked = true;

var series2 = chart.series.push(new am4charts.ColumnSeries());
series2.columns.template.width = am4core.percent(80);
series2.columns.template.tooltipText = "{name}: {valueY.value}";
series2.name = "Series 2";
series2.dataFields.categoryX = "category";
series2.dataFields.valueY = "value2";
series2.stacked = true;

var series3 = chart.series.push(new am4charts.ColumnSeries());
series3.columns.template.width = am4core.percent(80);
series3.columns.template.tooltipText = "{name}: {valueY.value}";
series3.name = "Series 3";
series3.dataFields.categoryX = "category";
series3.dataFields.valueY = "value3";
series3.stacked = true;

var series4 = chart.series.push(new am4charts.ColumnSeries());
series4.columns.template.width = am4core.percent(80);
series4.columns.template.tooltipText = "{name}: {valueY.value}";
series4.name = "Series 4";
series4.dataFields.categoryX = "category";
series4.dataFields.valueY = "value4";
series4.stacked = true;

chart.scrollbarX = new am4core.Scrollbar();

chart.exporting.menu = new am4core.ExportMenu();

The internal.js file

const puppeteer = require('puppeteer');

module.exports = async function (server) {

  let url = 'http://localhost:3000';
  
  // Open the browser
  const browser = await puppeteer.launch({
    // Run the browser headless or not
    headless: true
  });

  // Open a tab
  const page = await browser.newPage();

  // Set the referer. That is only needed if the server needs it for security reasons
  await page.setExtraHTTPHeaders({
    'referer': url
  });
  await page.setRequestInterception(true);
  page.on('request', request => {
    request.continue();
  });

  // Defines where the file should be saved
  await page._client.send('Page.setDownloadBehavior', {
    behavior: 'allow',
    // Using a folder at the root to save the images
    downloadPath: '/export'
  });

  // Define the URL where the chart is
  await page.goto(url);

  // Run scripts in the page
  await page.evaluate(() => {

    // Make the evaluate callback wait until the export is done
    return new Promise((resolve, reject) => {
    
      // Call the export on the chart
      chart.exporting.export('pdf').finally(() => {

        // Wait for the download to get done
        setTimeout(() => {
          resolve();
        }, 2000);
      });
    });
  });

  // Wait for the browser to close to stop the server
  browser.on('disconnected', async () => {

    // Stop the server
    await server.stop({
      timeout: 3000
    });
  });

  // Close the browser
  await browser.close();
};

Now run node server.js to start the server and to generate the chart.

Where to customize the export functionality

You can customize the code calling the export functionality inside the page.evaluate() method as shown below:

// Run scripts in the page
await page.evaluate(() => {

  // Make the evaluate callback wait until the export is done
  return new Promise((resolve, reject) => {
  
    // Call the export on the chart
    chart.exporting.export('pdf').finally(() => {

      // Wait for the download to get done
      setTimeout(() => {
        resolve();
      }, 2000);
    });
  });
});

Please check more about exporting here.

Download the example

Download the whole example here and run npm i to load the dependencies.