4đź‘Ť
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:
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):
- Unlimited chart generation
- Watermark
- Sub second request latency
- Google Premium Network
- Worldwide CDN
- WebP and Gif support
…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] }
]
}
}
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:
But, if you do all these, you can end up with some rather nice looking chart PDFs:
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>
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.