July 7, 2020 Update:
- The “See it in action” link below is updated to make it work again.
- The code is now available on Github repository as well, click here to access it.
- While this Apex Code and VF page approach still works in Classic and Lightning experience, there might be other ways to achieve this using Lightning Components. I will certainly write a new post if I get a chance to explore more on this.
Yes it is possible, even with new Google Dynamic Charts, it is possible*.
Earlier, generating Google Charts was a bit easy, as we just had to pass the data parameters in the URL and that’s it. As it used to generate the charts directly as image, it was easier to put them inside PDF as well. Unfortunately Google has deprecated them and may take them down any time. Hence we can’t rely on them. Anyway they had issue of data privacy as well.
For generating the new Google Charts we need write JavaScript code. While these are really great as far as customization is conerned, visualforce pages rendered as PDF destroys them! As we are aware of annoyances of PDF generation using Visualforce pages like limited Font support, CSS Support, etc. But the most annoying, it won’t execute any JavaScript in the page when rendered as PDF. Hence, our Visualforce page with Google Chart generation script is of no use.
After a bit of head scratching and discussing it with my friend, we have solved our issue. The solution below is useful if you are generating the PDF manually like via a button click.
Before you go through code, see it in action.
We will be showing a bar chart in PDF showing Sum of Amounts for Opportunity grouped by Close Date. In this example we have 3 different Visualforce pages. First one is simple landing page. Second page actually generates the Google Chart and saves it as PNG file to Documents in salesforce. Third one is our PDF page that displays the saved PNG image using apex:image tag.
We will be using a single apex controller for this example. The code for the same as given below. “insertChartImage” is the special method that we will explain later on in this post.
public class GraphPageController { public List<OppWrapper> wrList {get;set;} public String docId {get; set;} public GraphPageController() { wrList = new List<OppWrapper>(); //populating the list of wrapper wrList = populateWrapper(); docId = ApexPages.currentPage().getParameters().get('docId'); } //querying on Opportunity to get Sum of Amount Grouped by Close Date public static List<AggregateResult> fetchOppDetails() { return [select CloseDate, SUM(Amount) amtSum from Opportunity where Amount != null group by CloseDate order by CloseDate desc limit 10]; } //Wrapper Class for displaying data on VF page public class OppWrapper { public String closeDate {get;set;} public Double amtSum {Get;set;} public OppWrapper() { amtSum = 0; } } //method that populates list of wrapper public static List<OppWrapper> populateWrapper() { List<OppWrapper> owList = new List<OppWrapper>(); for(aggregateResult aggr : fetchOppDetails()) { OppWrapper ow = new OppWrapper(); ow.closeDate = Date.valueOf(aggr.get('CloseDate')).format(); ow.amtSum = Double.valueOf(aggr.get('amtSum')); owList.add(ow); } return owList; } //visualforce remoting method that is used to draw the chart @RemoteAction public static List<OppWrapper> fetchOppData() { return populateWrapper(); } //visualforce remoting method that inserts a document inside Salesforce @RemoteAction public static String insertChartImage(String base64String) { String returnVal = 'ERROR'; if(base64String != null) { //this line will remove "data:image/base64" string from the base64 String String imageString = base64String.substringAfter('base64,'); //converting the base64 string to Blob Blob docBody = EncodingUtil.base64Decode(imageString); //inserting a new document Document doc = new Document(); doc.Name = 'MyImage_'+System.Now().format(); doc.Body = docBody; doc.Type = 'png'; //putting the document to Personal Folder for logged in User doc.FolderId = UserInfo.getUserId(); insert doc; returnVal = doc.id; } return returnVal; } }
Below is the code for our landing page. It is a simple page showing Opportunity data grouped by Close Date in a table.
<!-- LANDING PAGE WHICH INITIATES THE ACTION --> <apex:page controller="GraphPageController"> <style> .alignCenter { text-align:center; padding:2px; } </style> <apex:sectionHeader title="Opportunity Table"/> <apex:form > <apex:pageblock mode="maindetail"> <apex:pageBlockTable value="{!wrList}" var="wr" style="width:30%;margin:auto;"> <apex:column headerValue="Close Date" width="125px" style="text-align:center;padding:2px;" headerClass="alignCenter"> <apex:outputText value="{!wr.closeDate}"/> </apex:column> <apex:column headerValue="Sum of Amount" width="125px" style="text-align:center;padding:2px;" headerClass="alignCenter"> <apex:outputText value="{!wr.amtSum}"/> </apex:column> </apex:pageBlockTable> </apex:pageblock> <p style="text-align:center;"> <apex:outputLink value="{!$Page.IntermediatePage}" style="padding: 5px;text-decoration: none;" styleclass="btn"> <apex:outputText value="See the beautiful chart in PDF!"/> </apex:outputLink> </p> </apex:form> </apex:page>
Below is the page which opens up when clicked on the button in our landing page. This page actually generates the chart and saves it as a PNG image to Salesforce.
<!-- INTERMEDIATE PAGE WHICH ACTUALLY GENERATES THE GOOGLE CHART --> <apex:page controller="GraphPageController" sidebar="false" showHeader="false" standardStylesheets="false"> <apex:includeScript id="a" value="https://www.google.com/jsapi" /> <p style="text-align:center;margin-top:200px;background-color:white;z-index:1000;"> <img style="margin:4px;" src="/img/loading.gif" title="Please Wait..." /><br/> <apex:outputText value="Generating Chart ... Please Wait ... "/> </p> <!-- Chart will be drawn in this Div, hiding the same as it is not required to be shown --> <div id="chartBlock" style="visibility:hidden;"/> <script type="text/javascript"> // Load the Visualization API and the piechart package. google.load('visualization', '1.0', {'packages':['corechart']}); // Set a callback to run when the Google Visualization API is loaded. google.setOnLoadCallback(initCharts); function initCharts() { Visualforce.remoting.Manager.invokeAction ( '{!$RemoteAction.GraphPageController.fetchOppData}', //fetching chart data function(result, event) { // load Column chart var oppChart = new google.visualization.ColumnChart(document.getElementById('chartBlock')); // Prepare table model for chart with columns var data = new google.visualization.DataTable(); data.addColumn('string', 'Close Date'); data.addColumn('number', 'Sum of Amount'); // add rows from the remoting results for(var i =0; i<result.length;i++) { var r = result[i]; data.addRow([r.closeDate, r.amtSum]); } //adding listener when Chart drawn is completed to generate base64 image string for the chart google.visualization.events.addListener(oppChart, 'ready', function () { Visualforce.remoting.Manager.invokeAction ( '{!$RemoteAction.GraphPageController.insertChartImage}', oppChart.getImageURI(), function(result, event) { if(result != null && result != 'ERROR') { //redirecting to PDF page with Document Id as URL parameter window.location.href = '/apex/ChartPagePDF?docId='+result; } }, {escape: true} ); }); //lets draw the chart with some options to make it look nice. oppChart.draw(data, { legend : { position: 'top' }, width : window.innerWidth, vAxis : { textStyle : {fontSize: 10} }, hAxis : {textStyle : {fontSize: 10}, showTextEvery : 1, slantedText : false } }); }, {escape:true} ); } </script> </apex:page>
Finally the PDF page which displays the saved chart image.
<!-- PDF PAGE --> <apex:page controller="GraphPageController" showHeader="false" standardStylesheets="false" renderAs="pdf" applyHTMLTag="false"> <head> <style> body { font-family: sans-serif; } .alignCenter { text-align:center; padding:2px; } @page { size: landscape; } </style> </head> <body> <h2> Opportunity Chart </h2> <apex:dataTable value="{!wrList}" var="wr" style="width:50%;margin:auto;border:1px solid black;"> <apex:column headerValue="Close Date" width="125px" style="text-align:center;padding:2px;" headerClass="alignCenter"> <apex:outputText value="{!wr.closeDate}"/> </apex:column> <apex:column headerValue="Sum of Amount" width="125px" style="text-align:center;padding:2px;" headerClass="alignCenter"> <apex:outputText value="{!wr.amtSum}"/> </apex:column> </apex:dataTable> <div style="clear:both;height:25px;"></div> <apex:image value="/servlet/servlet.FileDownload?file={!docId}" style="width:100%;"/> </body> </apex:page>
We won’t be covering explanation of line by line code here. Hence lets jump to below javascript in second visualforce page.
//adding listener when Chart drawn is completed to generate base64 image string for the chart google.visualization.events.addListener(oppChart, 'ready', function () { Visualforce.remoting.Manager.invokeAction ( '{!$RemoteAction.GraphPageController.insertChartImage}', oppChart.getImageURI(), function(result, event) { if(result != null && result != 'ERROR') { window.location.href = '/apex/ChartPagePDF?docId='+result; } }, {escape: true} ); });
We are adding a listener function that will be invoked whenever the Chart drawing is complete. When this function is invoked it will calling below method from our apex controller.
In javascript above we are using a standard method provided by Google Charts “getImageURI“. This method returns a string which is nothing but PNG image encoded in base64 string format. The apex controller’s “insertChartImage” method takes this string as parameter and converts it to a blob value using base64Decode function from EncodingUtils class, a Salesforce standard. Then we assign this blob as body for a document and insert the same to use it on PDF page. Once the document is inserted, the method returns its ID to JavaScript. Then the JavaScript method redirects User to PDF page passing Document Id as URL Parameter.
//visualforce remoting method that inserts a document inside Salesforce @RemoteAction public static String insertChartImage(String base64String) { String returnVal = 'ERROR'; if(base64String != null) { //this line will remove "data:image/base64" string from the base64 String String imageString = base64String.substringAfter('base64,'); //converting the base64 string to Blob Blob docBody = EncodingUtil.base64Decode(imageString); //inserting a new document Document doc = new Document(); doc.Name = 'MyImage_'+System.Now().format(); doc.Body = docBody; doc.Type = 'png'; //putting the document to Personal Folder for logged in User doc.FolderId = UserInfo.getUserId(); insert doc; returnVal = doc.id; } return returnVal; }
The document inserted above might not be useful after the PDF is generated, hence we will need to scheduled job to delete such documents timely. You might want to give it a try to do this without inserting a document.
While the above solution works fine for manual PDF generation, it might not work for scenario where the PDF is to be generated automatically, and that is why the asterisk (*) sign in title above.
Let me know if you tried it. I would love to hear feedback, suggestions in comments.
Reblogged this on SutoCom Solutions.
Reblogged this on SutoCom Solutions.
nice one Kshitij!!
Thank you anshul 🙂