AmCharts multi-chart export test

Benchmark created on


Description

Testing two different approaches to exporting multiple AmCharts charts as a single image.

Preparation HTML

<div id="omnidiv" style="width:100%;">
    <div id="piediv" style="width:450px; height:400px;"></div>
    <div id="linediv" style="width:450px; height:400px;"></div>
    <div id="columndiv" style="width:450px; height:400px;"></div>
</div>
<script src="http://www.amcharts.com/lib/3/amcharts.js"></script>
<script src="http://www.amcharts.com/lib/3/pie.js"></script>
<script src="http://www.amcharts.com/lib/3/serial.js"></script>
<script src="http://www.amcharts.com/lib/3/exporting/amexport.js"></script>
<script src="http://www.amcharts.com/lib/3/exporting/canvg.js"></script>
<script src="http://www.amcharts.com/lib/3/exporting/rgbcolor.js"></script>
<script src="http://www.amcharts.com/lib/3/exporting/jspdf.js"></script>
<script src="http://www.amcharts.com/lib/3/exporting/jspdf.plugin.addimage.js"></script>

Setup

window.saveAs = function() {};
    
    var pieChart, lineChart, columnChart;
    
    var exportConfig = {
      menuTop: "21px",
      menuBottom: "auto",
      menuRight: "21px",
      backgroundColor: "#efefef",
    
      menuItemStyle: {
        backgroundColor: '#EFEFEF',
        rollOverBackgroundColor: '#DDDDDD'
      },
    
      menuItems: [{
        textAlign: 'center',
        icon: 'http://www.amcharts.com/lib/3/images/export.png',
        title: 'export',
        items: [{
          title: 'JPG',
          format: 'jpg'
        }, {
          title: 'PNG',
          format: 'png'
        }, {
          title: 'SVG',
          format: 'svg'
        }, {
          title: 'PDF',
          format: 'pdf'
        }]
      }]
    };
    
    pieChart = AmCharts.makeChart("piediv", {
      dataProvider: [{
        category: 'A Slice',
        value: 60
      }, {
        category: 'Another Slice',
        value: 20
      }, {
        category: 'Not A Slice',
        value: 20
      }],
      exportConfig: exportConfig,
      titleField: "category",
      type: "pie",
      valueField: "value"
    });
    lineChart = AmCharts.makeChart("linediv", {
      categoryField: "category",
      dataProvider: [{
        category: "x = 1",
        value: 1
      }, {
        category: "x = 2",
        value: 4
      }, {
        category: "x = 3",
        value: 8
      }, {
        category: "x = 4",
        value: 16
      }],
      exportConfig: exportConfig,
      graphs: [{
        title: "y = x^2",
        type: "line",
        valueField: "value"
      }],
      type: "serial"
    });
    columnChart = AmCharts.makeChart("columndiv", {
      categoryField: "category",
      dataProvider: [{
        category: "2013-12-02",
        value: 2
      }, {
        category: "2013-12-03",
        value: 4
      }, {
        category: "2013-12-04",
        value: 8
      }, {
        category: "2013-12-05",
        value: 16
      }],
      exportConfig: exportConfig,
      graphs: [{
        title: "Things",
        type: "column",
        valueField: "value"
      }],
      type: "serial"
    });
    
    function exportMultiple(charts, format, fileName) {
      var extension;
      switch (format) {
        case 'svg':
          format = 'image/svg+xml';
        case 'image/svg+xml':
          extension = 'svg';
          break;
        case 'pdf':
          format = 'application/pdf';
        case 'application/pdf':
          extension = 'pdf';
          break;
        case 'png':
          format = 'image/png';
        case 'image/png':
          extension = 'png';
          break;
        case 'jpeg':
        case 'jpg':
          format = 'image/jpeg';
        case 'image/jpeg':
          extension = 'jpeg';
          break;
      }
    
      var images = [];
      var width = 0,
        height = 0;
      for (var i = 0; i < charts.length; i++) {
        var exporter = charts[i].AmExport;
        if (!exporter) {
          continue;
        }
    
        exporter.output({
          format: 'png',
          output: 'datastring'
        }, function(blob) {
          var image = document.createElement("img");
          image.src = blob;
          images.push(image);
        });
    
        width += charts[i].divRealWidth;
        height = charts[i].divRealHeight > height ? charts[i].divRealHeight : height;
      }
    
      var canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
    
      var context = canvas.getContext("2d");
      var currentX = 0,
        currentY = 0;
      for (i = 0; i < images.length; i++) {
        context.drawImage(images[i], currentX, currentY);
        currentX += images[i].naturalWidth;
      }
    
      var data = canvas.toDataURL(format);
      var tempExporter = new AmCharts.AmExport({
        addListener: function() {}
      });
      var blob = tempExporter.generateBlob(data, format);
    
      saveAs(blob, fileName + '.' + extension);
    }
    
    function generateOutput(cfg, callback) {
      var _this = this,
        charts = _this.charts,
        svgs = [],
        canvas = document.createElement('canvas'),
        context = canvas.getContext('2d'),
        offset = {
          x: 0,
          y: 0
        },
        tmp = {};
    
      _this.processing.buffer = [];
      _this.processing.drawn = 0;
      _this.canvas = canvas;
    
      var currentX = 0,
        width = 0,
        height = 0;
      for (var i = 0; i < charts.length; i++) {
        width += charts[i].divRealWidth;
        height = charts[i].divRealHeight > height ? charts[i].divRealHeight : height;
        var chartSvgs = charts[i].div.getElementsByTagName('svg');
        for (var j = 0; j < chartSvgs.length; j++) {
          var parentNode = chartSvgs[j].parentNode,
            svgX = Number(parentNode.style.left.slice(0, -2)),
            svgY = Number(parentNode.style.top.slice(0, -2));
          tmp = AmCharts.extend({}, offset);
    
          offset.x = svgX ? svgX : offset.x;
          offset.y = svgY ? svgY : offset.y;
    
          _this.processing.buffer.push([chartSvgs[j], AmCharts.extend({}, offset)]);
    
          if (svgY && svgX) {
            offset = tmp;
          } else {
            offset.y += svgY ? 0 : parentNode.offsetHeight;
          }
    
          svgs.push(chartSvgs[j]);
        }
        currentX += charts[i].divRealWidth;
        offset = {
          x: currentX,
          y: 0
        };
      }
    
      canvas.id = AmCharts.getUniqueId();
      canvas.width = width;
      canvas.height = height;
    
      if (cfg.backgroundColor || format == 'image/jpeg') {
        context.fillStyle = cfg.backgroundColor || '#FFFFFF';
        context.fillRect(0, 0, canvas.width, canvas.height);
      }
    
      function drawItWhenItsLoaded() {
        var img, buffer, offset, source;
    
        if (_this.processing.buffer.length == _this.processing.drawn) {
    
          return callback();
        } else {
          buffer = _this.processing.buffer[_this.processing.drawn];
          source = new XMLSerializer().serializeToString(buffer[0]); //source = 'data:image/svg+xml;base64,' + btoa();
          offset = buffer[1];
    
          if (cfg.render == 'browser') {
            img = new Image();
            img.id = AmCharts.getUniqueId();
            source = 'data:image/svg+xml;base64,' + btoa(source);
    
            img.onload = function() {
              context.drawImage(this, buffer[1].x, buffer[1].y);
              _this.processing.drawn++;
              drawItWhenItsLoaded();
            };
    
            img.onerror = function() {
              context.drawImage(this, buffer[1].x, buffer[1].y);
              _this.processing.drawn++;
              drawItWhenItsLoaded();
            };
            img.src = source;
    
            if (img.complete || typeof(img.complete) == 'undefined' || img.complete === undefined) {
              img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
              img.src = source;
            }
          } else if (cfg.render == 'canvg') {
            canvg(canvas, source, {
              offsetX: offset.x,
              offsetY: offset.y,
              ignoreMouse: true,
              ignoreAnimation: true,
              ignoreDimensions: true,
              ignoreClear: true,
              renderCallback: function() {
                _this.processing.drawn++;
                drawItWhenItsLoaded();
              }
            });
          }
        }
      }
      return drawItWhenItsLoaded();
    }
    
    function hopefullyFasterExportMultiple(charts, format) {
      var exporter = new AmCharts.AmExport({
        addListener: function() {}
      });
      var temp = exporter.generateOutput;
      exporter.charts = charts;
      exporter.generateOutput = generateOutput;
      exporter.output(format);
    }

Test runner

Ready to run.

Testing in
TestOps/sec
Constructing a new canvas from the three charts' individual outputs
exportMultiple([pieChart, lineChart, columnChart], 'png', 'stuff');
ready
Rewriting the generateOutput function to take multiple charts
hopefullyFasterExportMultiple([pieChart, lineChart, columnChart], 'png');
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.