vue.jschart.jsjspdfvue-chartjsjsreport

Convert charts from vue-chartjs to a PDF


I'm trying to make a report system with charts are drawn by vue-chartjs, and I wanted to download them as a PDF, or any file.

Here is what App.vue looks like

<template>
  <div>
  <div id="printSection" ref="document">
    <AreaChart />
    <PieChart />
         ...
  </div>
   
        <button @click="printSection">Print Shit</button>
    </div>
</template>

<script>
import AreaChart from "./SuperAdmin/AreaChart";
import PieChart from "./SuperAdmin/PieChart";
                 ...
//import { Printd } from "printd";  //tried with this one
//import html2pdf from 'html2pdf.js';  // and this one
export default {
  name: "App",
  components: {
    AreaChart,
    PieChart,
  },
  data() {
    return { };
  },
  mounted() { 
    
   var d = new Printd();
  },
  methods: {
    printSomething () {   // this was for printd
      this.d.print( this.$el, this.cssText)
    },
    exportToPDF () {     // this was for html2pdf
        html2pdf()
                html2pdf(this.$refs.document, {
                    margin: 1,
                    filename: 'document.pdf',
                    image: { type: 'jpeg', quality: 0.98 },
                    html2canvas: { dpi: 192, letterRendering: true },
                    jsPDF: { unit: 'in',  orientation: 'landscape' }
        })
      },
      printSection() {
        this.$htmlToPaper("printSection");  // this was for htmltopaper
      }
  }
};
</script>

<style>
    ...
</style>

So, as you can see, I have tried 3 packages to convert the charts to PDFs but they always print empty papers, except for html2pdf, which cuts the graphs in half.

Here is one of my charts:

PieChart.vue

<script>
import { Pie } from "vue-chartjs";
import displayshit from '../../service/Hello'
import labels from 'chartjs-plugin-labels'
//import { plugins } from 'chart.js';
export default {
  extends: Pie,
  async mounted() {
    var data = [];
    var new_data = [];
    var Type = ['Mobile','Card','Both']
    var sum = 0 ;
      data = (await displayshit.displayanothershit()).data.message;
   var  Mobile_number = data.find(element=> JSON.stringify(element._id) == JSON.stringify([Type[0]]))  || 0
   var  Card_number = data.find(element=> JSON.stringify(element._id) == JSON.stringify([Type[1]])) || 0  
   var  Both_number = data.find(element=> element._id.length == 2) || 0    
     new_data.push(Mobile_number.count || 0)
     new_data.push(Card_number.count || 0)
     new_data.push(Both_number.count || 0)

    this.gradient = this.$refs.canvas
      .getContext("2d")
      .createLinearGradient(0, 0, 0, 450);
    this.gradient2 = this.$refs.canvas
      .getContext("2d")
      .createLinearGradient(0, 0, 0, 450);

    this.gradient.addColorStop(0, "rgba(255, 0,0, 0.5)");
    this.gradient.addColorStop(0.5, "rgba(255, 0, 0, 0.25)");
    this.gradient.addColorStop(1, "rgba(255, 0, 0, 0)");

    this.gradient2.addColorStop(0, "rgba(0, 231, 255, 0.9)");
    this.gradient2.addColorStop(0.5, "rgba(0, 231, 255, 0.25)");
    this.gradient2.addColorStop(1, "rgba(0, 231, 255, 0)");
   this.renderChart(
      {
        labels: Type,
        datasets: [
          {
            backgroundColor: [this.gradient, this.gradient2, "#00D8FF"],
            data: new_data
          },
        ],

      },
 
     {  responsive: true,  maintainAspectRatio: false,plugins: { 
    labels: {render: 'label',
  
  }
    }}
   );
    

  },
};
</script>

I want to convert the charts to PDF files, but I also want to be able to choose which charts to include in a PDF.

Here is a sandbox with my code.

Also, if you could explain how to update charts reactively, that would be great


Solution

  • Apparently, (locally) converting charts (to PDFs) from chart.js is actually not that easy, and converting charts from vue-chartjs is even more difficult. Luckily, there's (at least) two ways to do so, though they aren't very optimal.

    1) The Easy Way(s)

    image-charts.com provides an API that can accept chart data in chart.js format and convert it to an image, but it doesn't seem to be able to convert it to a PDF.

    For instance, you can pass this data as a URL to image-charts:

    https://image-charts.com/chart.js/2.8.0?
        bkg=white
        &c= {
            type: 'line',
            data: { labels: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug'],
                datasets: [
                    { 
                        backgroundColor: 'rgba(255,150,150,0.5)', 
                        borderColor:'rgb(255,150,150)',
                        data:[-23,64,21,53,-39,-30,28,-10],
                        label:'Dataset',fill:'origin'
                    }
                ]
            }
           }
    
    

    ...and you get this back:

    image-charts-demo

    It's free to use, with no rate limiting, but there is a watermark in the top-right corner, as you can see in the image above. The free plan includes (currently):

    ...but you can check out their pricing for more info and other plans. Also check out their docs for further information and play around in their sandbox to see if it's what you need.

    Alternately, Quickchart.io also provides an easy-to-use API that accepts chart data, and will output a PDF (or other formats) for you to use. Simply make a request to the /chart endpoint with your chart data/formatting options, and you've got a PDF output:

    https://quickchart.io/chart?
        format=PDF
        &bkg=white
        &c= { 
            type: 'bar', 
            data: { 
                labels: ['Q1', 'Q2', 'Q3', 'Q4'], 
                datasets: [
                    { label: 'Users', data: [50, 60, 70, 180] }, 
                    { label: 'Revenue', data: [100, 200, 300, 400] }
                ] 
            }
        } 
    

    View PDF here

    You can play around in their sandbox, and check out their docs here for more info, but note that the API is rate limit enforced, unless you pay for the premium plans. There's also a node.js/JS module you can import and use in your project here.

    While there is no watermark, and you do get a PDF output, image-charts is still probably a better solution if your app serves many users and you don't feel like paying for the premium plans, since they (image-charts) don't rate limit requests.

    2) The Free & Local & Way too Difficult Way

    For an approach without an external API, that can run offline and locally, you can use html2canvas and jsPDF to render charts to PDFs. It's significantly more complicated, especially with vue-chartjs, because you have to specifically place the chart in a container of 594px by 459px (for letter size paper), and make sure that the chart's options are set to

    { responsive: true, maintainAspectRatio: false }
    

    ...or the image will either be stretched or squashed or too small or too large, like this:

    failure 1 failure 2 failure 3 failure 4

    But, if you do all these, you can end up with some rather nice looking chart PDFs:

    success 1 success 2

    I think that the easiest way to export charts to PDFs is to create a module that can be imported wherever you need to export them:

    exporter.js

    import html2canvas from "html2canvas";
    import jsPDF from "jspdf";
    
    export default class Exporter {
      constructor(charts) {
        this.charts = charts;
        this.doc = new jsPDF("landscape", "px", "letter");
      }
      export_pdf() {
        return new Promise((resolve, reject) => {
          try {
            this.charts.forEach(async (chart, index, charts) => {
              this.doc = await this.create_page(chart, this.doc);
              if (index === charts.length - 1) resolve(this.doc); // this.doc.save("charts.pdf"); // <-- at the end of the loop; display PDF
              this.doc.addPage("letter");
            });
          } catch (error) {
            reject(error);
          }
        });
      }
    
      async create_page(chart, doc) {
        const canvas = await html2canvas(chart, {
          scrollY: -window.scrollY,
          scale: 5 // <-- this is to increase the quality. don't raise this past 5 as it doesn't get much better and just takes longer to process
        });
        const image = canvas.toDataURL("image/jpeg", 1.0),
          pageWidth = doc.internal.pageSize.getWidth(),
          pageHeight = doc.internal.pageSize.getHeight(),
          ratio = (pageWidth - 50) / canvas.width,
          canvasWidth = canvas.width * ratio,
          canvasHeight = canvas.height * ratio,
          marginX = (pageWidth - canvasWidth) / 2,
          marginY = (pageHeight - canvasHeight) / 2;
    
        doc.addImage(image, "JPEG", marginX, marginY, canvasWidth, canvasHeight);
    
        return doc;
      }
    }
    
    

    You can import this in App.vue, or wherever you have your charts, and create a new instance of Exporter, with an array of chart elements to export, then call the export_pdf() method on it and do whatever you want with the PDF:

    let area = document.getElementById("area");
    let line = document.getElementById("line");
    
    const exp = new Exporter([line, area]);
    exp.export_pdf().then((pdf) => pdf.save("charts.pdf"));
    

    This way you can render as many or as few charts as you want.

    App.vue example:

    import Exporter from "@/assets/exporter.js";
    ...
    
    export default {
      methods: {
        exportToPDF() {
          let bar = document.getElementById("bar");
          let radar = document.getElementById("radar");
          let pie = document.getElementById("pie");
          let area = document.getElementById("area");
          let line = document.getElementById("line");
    
          const exp = new Exporter([line, bar, radar, pie, area]);
          exp.export_pdf().then((pdf) => pdf.save("charts.pdf"));
       }
       ...
    }
    

    Now, do note that, as aforementioned, the chart element you pass into Exporter will have to be contained within an element of 594px by 459px, since that's the paper's size, and is the optimum size for the chart. But then you won't be able to resize the chart on your website or such, so what you can do, is create two identical charts, one which you can do whatever you want with, and another, contained in a div of the right size, but placed off screen, so the user doesn't see it.

    Something like this:

    <template>
        <BarChart />
        <div class="hidden">
            <BarChart id="bar" />
        </div>
    </template>
    
    <script>
        export default { STUFF GOES HERE}
    </script>
    
    <style>
        .hidden {
          width: 594px !important;
          height: 459px !important;
          position: absolute !important;
          left: -600px !important;
        }
    </style>
    

    example1

    The other chart is hidden off to the left where you can't see it.

    Both charts would be populated with the same data, but when you export to PDF, the off-screen chart would be converted to a PDF, not the one that the user would see. This is overly complicated, but its the only way I could find. If you try to hide charts with v-if or v-show, when you show the hidden chart, the other one will be deleted for some reason. Perhaps you could create a single component that would create the two charts together, to make things easier, but that would require further work.

    Also note that you might have to increase the distance the hidden chart is moved left if it is still showing on screen.

    Make sure to install the dependencies:

    npm i html2canavs jspdf
    

    I've created a node.js module for exporter.js which you can install and use alternatively. See here for installation instructions and more info

    How to refresh charts with vue-chartjs

    This is pretty simple actually. vue-chartjs has two mixins that you can use to achieve this, reactiveProp, and reactiveData. If you want to pass dynamic data through to a chart component that you've created as a prop, you can use reactiveProp:

    Linechart.js

    import { Line, mixins } from 'vue-chartjs'
    const { reactiveProp } = mixins
    
    export default {
      extends: Line,
      mixins: [reactiveProp],
      props: ['options'],
      mounted () {
        // this.chartData is created in the mixin.
        // If you want to pass options please create a local options object
        this.renderChart(this.chartData, this.options)
      }
    }
    // from https://vue-chartjs.org/guide/#updating-charts
    

    Wherever you want to display the chart, just import Linechart.js, and register it as a component:

    import LineChart from './LineChart.js'
    
    export default {
        components: {
            LineChart
        },
        ...
    }
    

    ...then place the chart in your template, and pass the data over through the chart-data prop:

    <line-chart :chart-data="datacollection"></line-chart>
    

    It will automatically update when datacollection changes.

    Code examples and a great deal of more in-depth info about this is available on the docs.

    View a demo of both chart updating and converting them to PDFs here.