14
votes

I have used chart.js to generate a report page that has multiple charts. I need to export this report to PDF. There are many solutions available via search, but I cannot find one which has multiple canvas elements.

The only available solution seems to be to loop through all the images, and recreate the report using the images, and then download that as a pdf.

Is there any simpler/more efficient way to accomplish this?

<body>
<h1> Chart 1 </h1>
<div style="width:800px; height:400px;">
<canvas id="chart_1" width="50" height="50"></canvas>
</div>

<h1> Chart 2 </h1>
<div style="width:800px; height:400px;">
<canvas id="chart_2" width="50" height="50"></canvas>
</div>

<h1> Chart 3 </h1>
<div style="width:800px; height:400px;">
<canvas id="chart_3" width="50" height="50"></canvas>
</div>
</body>
2

2 Answers

25
votes

Honestly, it seems like the easiest approach would be to just provide a "download to PDF" link that pops up the browser's print page and instruct to user to select "print as pdf".

If that approach doesn't work for you (or your users), then here is a rough way to do it.

Basically, we create a new canvas element that is the size of your report page and incrementally paint the pixels from your existing chart.js canvas charts into the new canvas. Once that is done, then you can use jsPDF to add the new canvas to a pdf document as an image and download the file.

Here is an example implementation that does just that.

$('#downloadPdf').click(function(event) {
  // get size of report page
  var reportPageHeight = $('#reportPage').innerHeight();
  var reportPageWidth = $('#reportPage').innerWidth();

  // create a new canvas object that we will populate with all other canvas objects
  var pdfCanvas = $('<canvas />').attr({
    id: "canvaspdf",
    width: reportPageWidth,
    height: reportPageHeight
  });

  // keep track canvas position
  var pdfctx = $(pdfCanvas)[0].getContext('2d');
  var pdfctxX = 0;
  var pdfctxY = 0;
  var buffer = 100;

  // for each chart.js chart
  $("canvas").each(function(index) {
    // get the chart height/width
    var canvasHeight = $(this).innerHeight();
    var canvasWidth = $(this).innerWidth();

    // draw the chart into the new canvas
    pdfctx.drawImage($(this)[0], pdfctxX, pdfctxY, canvasWidth, canvasHeight);
    pdfctxX += canvasWidth + buffer;

    // our report page is in a grid pattern so replicate that in the new canvas
    if (index % 2 === 1) {
      pdfctxX = 0;
      pdfctxY += canvasHeight + buffer;
    }
  });

  // create new pdf and add our new canvas as an image
  var pdf = new jsPDF('l', 'pt', [reportPageWidth, reportPageHeight]);
  pdf.addImage($(pdfCanvas)[0], 'PNG', 0, 0);

  // download the pdf
  pdf.save('filename.pdf');
});

You can see it in action at this codepen.

Now let's talk about some gotchas with this approach. First, you have to control the position of each chart.js canvas in the new canvas object. The only way to do that is to have an understanding of how your report page is structured and implement that same structure. In my example, my charts are in a 2x2 grid and the logic handles this accordingly. If you had a 3x2 grid or something different then you would have to change the positioning logic.

Lastly, the final pdf output file dimensions are much larger than the original chart page (from the web). I think the reason is because my chart "container" div stretches across the full page. Therefore, you probably want to use a different approach for setting the size of your new canvas.

So long story short, the above example is meant to demonstrate an approach and not be your final solution.

Good luck!

0
votes

I have a working solution in vanilla javascript(although I used ts typing) and using the lib jsPdf, where you need a plot per pdf page:

let index = 1;
// create new pdf object
// if don't choose compress as true you will end up with a large pdf file
let pdf = new jsPDF({
  orientation: 'landscape',
  unit: 'px',
  format: 'a4',
  compress: true,
})
// search for the html element(s) you need
const canvas = document.querySelectorAll("canvas");
// here my size are in pixels since I configured that in the obj instance
let pageWidth = 400; 
let pageHeight = 400;
let index = 1;
// traverse the array of canvas 
canvas.forEach( (canva:HTMLCanvasElement) => {

  // I added some options among others I added the type of the compression
  // method: FAST
  pdf.addImage(canva, 'PNG', 10, 10, pageWidth, pageHeight, `img${index}`, "FAST");
  // so as to not end up with an extra pdf page at the end of the iteration
  if (index < canvas.length) {
    pdf.addPage();
  }
  index++;
});

// download the pdf
pdf.save('Reporte.pdf');