Generate Quick PDF Charts in Rails 6 with Dhalang

Generate Quick PDF Charts in Rails 6 with Dhalang

It gets messier when you try to render charts and graphs on PDF, as even widely used gem like wickedPDF doesn’t properly support HTML canvas element without applying patches, and becomes headache to upgrade in long run. Today I’ll tell you a quick way to build rich PDF charts in Rails using Google’s headless engine Puppeteer and chartJS library (or highcharts etc). There is a ruby wrapper of it with the name of Dhalang. Which is quite easy to setup.

Installation:

Without any further due, lets start. Set up new rails app and add gem to Gemfile.

rails _6.0.0_ new dhalang_demo
gem 'Dhalang'
bundle install

You need to install puppeteer and chartJS

Run yarn add puppeteer && yarn add chart.js to do so (this will take around 1 to 2 min to complete)

# package.json
{
  "name": "dhalang_demo",
  "private": true,
  "dependencies": {
    "@rails/actioncable": "^6.0.0-alpha",
    "@rails/activestorage": "^6.0.0-alpha",
    "@rails/ujs": "^6.0.0-alpha",
    "@rails/webpacker": "^4.0.7",
    "chart.js": "^2.8.0",
    "puppeteer": "^1.19.0",
    "turbolinks": "^5.2.0"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.8.0"
  }
}

Now run yarn install to install and load the package.

Now heads over to application.js and import the library at bottom of all require statements. Your file will look like this

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

import "chart.js"

Now its time for views. Create controller with name home and set root path.

rails g controller home index

Set up routes,

# config/routes.rb

Rails.application.routes.draw do
  root "home#index"
  controller :home do
  	get :convert
  	get :pdf
  end
end

Creating Bubble Chart:

Lets create a sample bubble chart with a button for PDF Conversion. Put some HTML in index.html and create one similar file for PDF view. Lets name is pdf.html.erb

# views/home/index.html.erb

<div class="" style="padding: 30px; margin: auto; text-align: center;">
	<%= link_to "CONVERT TO PDF", convert_path, target: "_blank" %>
</div>
<canvas id="sample"></canvas>
<script type="text/javascript">
	<%= render "home/partials/main.js"%>
</script>
# views/home/pdf.html.erb

<canvas id="sample"></canvas>

<script type="text/javascript">
	<%= render "home/partials/main.js"%>
</script>

Create a partial to put the view specific JS code.

# views/home/partials/_main.js.erb

var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var config = {
	type: 'line',
	data: {
		labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
		datasets: [{
			label: 'My First dataset',
			borderColor: "#C6C22F",
			backgroundColor: "#C6C22F",
			data: [
				44,
				55,
				20,
				21,
				89,
				52,
				33
			],
		}, {
			label: 'My Second dataset',
			borderColor: "#4DAA33",
			backgroundColor: "#4DAA33",
			data: [
				24,
				55,
				60,
				11,
				89,
				82,
				73
			],
		}, {
			label: 'My Third dataset',
			borderColor: "#C559A5",
			backgroundColor: "#C559A5",
			data: [
				41,
				55,
				94,
				61,
				81,
				52,
				30
			],
		}, {
			label: 'My Third dataset',
			borderColor: "#45CCB7",
			backgroundColor: "#45CCB7",
			data: [
				23,77,58,43,45,75,32
			],
		}]
	},
	options: {
		responsive: true,
		title: {
			display: true,
			text: 'Chart.js Line Chart - Stacked Area'
		},
		tooltips: {
			mode: 'index',
		},
		hover: {
			mode: 'index'
		},
		scales: {
			xAxes: [{
				scaleLabel: {
					display: true,
					labelString: 'Month'
				}
			}],
			yAxes: [{
				stacked: true,
				scaleLabel: {
					display: true,
					labelString: 'Value'
				}
			}]
		}
	}
};

window.onload = function() {
	var ctx = document.getElementById('sample').getContext('2d');
	window.myLine = new Chart(ctx, config);
};

In home controller define 2 methods convert and pdf. Former for requesting Dhalang to process the page and later is the actual path to snapshot.

# controllers/home_controller.rb

class HomeController < ApplicationController
  def index
  end

  def pdf
  	# this method will be called by Dhalang
  end

  def convert
  	_url = request.base_url + pdf_path
  	_pdf = Dhalang::PDF.get_from_url(_url)
  	_file_name = "Report" 
        File.open("#{Rails.root}/public/#{_file_name}.pdf", "w+b") << _pdf
	redirect_to "/#{_file_name}.pdf"
  end
end

All set up. run rails server rails s . Head over to localhost:3000 and boom.

chartJS  with Dhalang

PDF Conversion:

Click on CONVERT TO PDF link and the page will be redirected to newly created PDF file.

chartJS PDF generation with Dhalang

Troubleshooting:

All cool. NO !!!

If you look closely, the graph is little bit stretched and blurry. It’s because we haven’t provided any configuration or view port settings.

For that, create a file under app/javascripts/controller/pdfSettings.js and put below code.

# app/javascripts/controller/pdfSettings.js

const createPdf = async() => {
  module.paths.push(process.argv[4]);
  const puppeteer = require('puppeteer');
  let browser;
  try {
    browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
    const page = await browser.newPage();
    await page.goto(process.argv[2], {
      timeout: 10000, 
      waitUntil: 'networkidle2'
    });
    await page.waitFor(250);
    await page.setViewport({width: 1000, height: 2950, deviceScaleFactor: 2});
    await page.pdf({
      path: process.argv[3],
      format: 'A4',
      padding: { top: 0, right: 0, bottom: 0, left: 0 },
      margin: { top: 0, right: 0, bottom: 0, left: 0 },
      printBackground: true
    });
  } catch (err) {
      console.log(err.message);
  } finally {
    if (browser) {
      browser.close();
    }
    process.exit();
  }
};
createPdf();

You should override and play with data in only 2 blocks naming page.setViewport and page.pdf . But you need to load this file at app start. A quick solution is to provide the path in application.rb after config.load_defaults 6.0 like below. You need to restart the app server after every change made to this file.

# config/application.rb
Dhalang::PDF::PDF_GENERATOR_JS_PATH = "#{Rails.root}/app/javascripts/controller/pdfSettings.js"

You might also need to adjust canvas height and width attributes accordingly, but only in rare case hence you will get sharp chart with no blur.

PS: Please don’t miss any step in installation otherwise you might run into problem. The most common error you might face is UnhandledPromiseRejectionWarning which can be resolved by following proper steps provided.

Bonus:

  1. Dhalang doesn’t supports query string parameters (i.e. ?key=value). So, you need to convert them into rails specific URL params (i.e. /:key1/:key2)
  2. If you are on CentOS and getting errors for library dependencies while installing puppeteer, try below command to install them all.
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y

For more trouble shooting refer this issue https://github.com/GoogleChrome/puppeteer/issues/391

Thanks. If you want to shout something, leave it in comments below. Anyone looking for source code, can get here. If you want to explore more ways to generate PDF charts with chartJS. Refer to my other article Generate PDF Charts in Rails 6

One Reply to “Generate Quick PDF Charts in Rails 6 with Dhalang”

Leave a Reply