Embedding Canvas and SVG charts in emails
April 30th, 2013In our app Freckle Time Tracking we’re sending out weekly reports to users by email, reporting to them what they achieved last week.
Our emails not only include a report, listing all entries they logged, but also our “Mini-Pulse”, a graphical representation of how much they worked for each project.
Here’s how a typical email we send out looks like:
Now, generating a table and styling it for the HTML email is (relatively) easy, and beyond the scope of this article, but let’s have look at the charts in the email. We already generate the Mini-Pulse graph in our web app—we use an ancient version of Raphael.js to generate SVG, and this gets the job done nicely (SVG works on practically every modern browser, and Raphael falls back to VML on older Internet Explorer versions). Of course, you could use Canvas or any other HTML supported by WebKit just as well.
This is all great in your web browser, but not for emails. First off, obviously, JavaScript
is disabled in HTML emails, so we can’t use that to generate the SVG on the fly; moreover
SVG only works in a handful of email clients.
The only image formats that reliably work in HTML emails are GIF, PNG and JPEG, which means we have to dynamically generate such an image containing the charts and refer to it from the email.
There’s two possible ways to do this:
- Reimplement the logic and rendering with a tool specifically made for generating chart images
- Reuse the existing JavaScript/SVG code in a headless web browser and make “screenshots”
We chose to reuse the code we have, so we can easily adapt and extend both the web app and the HTML emails in the future (plus no need to learn yet another tool!).
A great way to create screenshots of the graphs is to use PhantomJS, which is a headless WebKit with an API that has support for taking screenshots.
We also have the following requirements for our report emails:
- Don’t generate the Mini-Pulse if the email is never opened, to conserve server resources
- Cache the generated image once the email was opened once
- Securely serve the image and use encrypted URLs with embedded authentication (the user the email was sent to may no longer have permission to access Freckle at the time the email is opened)
- Charts should be retinafied (your HTML emails are retinafied, are they?)
- File size of image should be small so it loads fast on mobile email clients
To fulfill these requirements, here’s what happens when a user opens an email that has a chart embedded:
- HTML email is shown
- Email client or browser accesses URL in the form of https://app.letsfreckle.com/m/xxxx/yyy.gif
- If there’s a cached version of the image serve it and go to step 10, otherwise continue to step 4
- Rails app decrypts account ID, user ID, chart type and date range from the given encrypted URL*
- Rails app calls internal PhantomJS web service with a URL to call to generate the chart
- PhantomJS web service calls the Freckle Rails app internally
- Rails app serves Raphael.js, and our chart generation code and the data needed for it. (We use a special, stripped-down layout that only serves the chart and doubles resolution on everything to simulate rendering on a high-density screen (a feature that Raphael.js doesn’t yet directly support).
- PhantomJS renders the page
- PhantomJS returns a PNG to Rails (the call from step 4)
- Rails returns the PNG and caches it into a file (Rails page caching)
- Email client or web browser renders the PNG
All this sounds pretty complex, but it’s actually implemented in just about a hundred lines of code.
There’s a few tricky things you have to deal with when installing Phantom.js on a Linux server, such as adding fonts that may not be part of your default Linux server setup, but it’s pretty easy to get going. For Ubuntu 10.04, you can check out this gist with instructions on getting decent rendering quality.
*To accommodate passing parameters along from an HTML email to our Rails app, I’ve released URLcrypt, an open-source, MIT-licensed Ruby library for ‘elegant’ encrypted URLs. Alternatively, you could also use a table the holds tokens, but I find encrypting the account/user id information a more scalable solution.
Tweet