Vaadin 7 HighCharts Portlet

Introduction

So the last few Vaadin posts were on some basic introductory stuff, but not a whole lot of fun.  I thought I'd do a blog post on a quick and fun little Vaadin 7 portlet, one that uses HighCharts.

So I'm creating a portlet to show a pie chart of registered user ages (based on birthdate).  The fun part about this portlet is that it will use the Liferay API to access the user birthdates, but will use a DynamicQuery to check the birthdates, and we'll break it up into seven groups and display as a pie chart using HighCharts.  And the whole thing will be written for the Vaadin framework.

Project Setup

So first thing you need is the HighCharts for Vaadin 7 Add On.  You can use the Vaadin 7 Control Panel to deploy the Add On to your environment.  This Add On includes the Vaadin 7 widget for integrating with HighCharts in your Vaadin application, but you also need the HighCharts JavaScript file.

Initially I tried to use the version that comes with the Add On but quickly found that it was not going to work.  The JavaScript shipped with the Add On uses jQuery, but not jQuery in no conflict mode.  So that version of the HighCharts JavaScript won't work in the portal.

Fortunately, however, there is a viable alternative.  From the HighCharts Download page, one of the adapter options includes "Standalone framework" mode.  This adapter option does not rely on jQuery or any other JavaScript framework.

HighCharts Download

Select the Standalone Framework option and any other portions to include in the download you want.  The portlet being built in this blog uses the Pie Chart, so be sure to include that chart type.  I would selecting everything, that way you'll have a complete HighCharts JavaScript file to use in future charting portlets.

If you do not want the minified JavaScript, uncheck the "Compile code" checkbox before downloading.

Download the JavaScript file and save it for later.

Creating the Project

Since I'm using Intellij, I started a Maven project using the "liferay-portlet-archetype", similarly to the project I did for the "vaadin-sample-portlet" project.  I called the portlet and the project "vaadin-user-age-chart-portlet".

To create the portlet, there will be four classes:

  • UsersChartUI - This is the UI class for the portlet, it extends com.vaadin.ui.UI class.
  • DateRange - This is a utility class that calculates and keeps track of a date range.
  • UserAgeData - This is a utility class which retrieves the age data from the Liferay API.
  • UserPieChart - This is the extension class responsible for rendering the pie chart.

DateRange

Let's start with the DateRange class.  It basically has the following:

package com.dnebinger.liferay.vaadin.userchart; 
import java.io.Serializable; import java.util.Calendar; import java.util.Date; 
/**  * DateRange: A container for a date range.  * Created by dnebinger on 2/6/15.  */ public class DateRange implements Serializable{  private static final long serialVersionUID = 58547944852615871L;  private Calendar startDate;  private Calendar endDate; 
 /**  * DateRange: Constructor for the instance.  * @param type  */  public DateRange(final int type) {  super(); 
 endDate = Calendar.getInstance();  startDate = Calendar.getInstance(); 
 switch (type) {  case UserAgeData.AGE_0_10:  // end date is now 
 // start date is just shy of 11 years ago.  startDate.add(Calendar.YEAR, -11);  break;  case UserAgeData.AGE_11_20:  endDate.add(Calendar.YEAR, -11); 
 startDate.add(Calendar.YEAR, -21);  break;  case UserAgeData.AGE_21_30:  endDate.add(Calendar.YEAR, -21); 
 startDate.add(Calendar.YEAR, -31);  break;  case UserAgeData.AGE_31_40:  endDate.add(Calendar.YEAR, -31); 
 startDate.add(Calendar.YEAR, -41);  break;  case UserAgeData.AGE_41_50:  endDate.add(Calendar.YEAR, -41); 
 startDate.add(Calendar.YEAR, -51);  break;  case UserAgeData.AGE_51_60:  endDate.add(Calendar.YEAR, -51); 
 startDate.add(Calendar.YEAR, -61);  break;  case UserAgeData.AGE_60_PLUS:  endDate.add(Calendar.YEAR, -61); 
 startDate.add(Calendar.YEAR, -121);  break;  } 
 startDate.add(Calendar.DATE, 1);  } 
 /**  * getEndDate: Returns the end date in the range.  * @return Date The end date.  */  public Date getEndDate() {  // start by verifying the day of year comparisons  Calendar cal = Calendar.getInstance(); 
 if (cal.get(Calendar.DAY_OF_YEAR) != endDate.get(Calendar.DAY_OF_YEAR)) {  // update start and end days  endDate.add(Calendar.DATE, 1);  startDate.add(Calendar.DATE, 1);  } 
 return endDate.getTime();  } 
 /**  * getStartDate: Returns the start date in the range.  * @return Date The start date.  */  public Date getStartDate() {  getEndDate(); 
 return startDate.getTime();  } } 

So the class is initialized for a type code and, given the type code, it sets up the start and end dates to cover the appropriate range.  Each time the starting and end dates are retrieved, the dates will be checked for possible increment (to keep the ranges intact).

UserAgeData

The UserAgeData class uses the DynamicQuery API to count users that fall between the start and end date ranges.

package com.dnebinger.liferay.vaadin.userchart; 
import com.liferay.portal.kernel.dao.orm.DynamicQuery; import com.liferay.portal.kernel.dao.orm.DynamicQueryFactoryUtil; import com.liferay.portal.kernel.dao.orm.RestrictionsFactoryUtil; import com.liferay.portal.kernel.exception.SystemException; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.util.PortalClassLoaderUtil; import com.liferay.portal.model.Contact; import com.liferay.portal.service.UserLocalServiceUtil; 
/**  * class UserAgeData: A class that contains or determines the user age data percentages.  */ public class UserAgeData {  private static final Log logger = LogFactoryUtil.getLog(UserAgeData.class); 
 // okay, we need to know when the data was retrieved  private long timestamp; 
 // and we need a container for the data  private double[] percentages; 
 private static final long ONE_DAY_MILLIS = 1000 * 1 * 60 * 60 * 24; 
 public static final int AGE_0_10 = 0;  public static final int AGE_11_20 = 1;  public static final int AGE_21_30 = 2;  public static final int AGE_31_40 = 3;  public static final int AGE_41_50 = 4;  public static final int AGE_51_60 = 5;  public static final int AGE_60_PLUS = 6; 
 private DateRange[] ranges = new DateRange[7]; 
 /**  * UserAgeData: Constructor.  */  public UserAgeData() {  super(); 
 percentages = new double[7];  timestamp = 0; 
 for (int idx = 0; idx < 7; idx++) {  ranges[idx] = new DateRange(idx);  }  } 
 /**  * getPercentages: Returns either the cached percentages or pulls the count.  * @return double[] An array of 7 doubles with the age ranges.  */  public double[] getPercentages() {  // if we already have cached users  if (timestamp > 0) {  // need to check the timestamp  long current = System.currentTimeMillis(); 
 if (current <= (timestamp + ONE_DAY_MILLIS)) {  // we have values and they are still valid for caching 
 if (logger.isDebugEnabled()) logger.debug("Found user data in cache, returning it."); 
 return percentages;  }  } 
 // if we get here then either we have no info or the info is stale. 
 long[] counts = new long[7];  long totalUsers = 0; 
 // get the count of users  for (int idx = 0; idx < 7; idx++) {  counts[idx] = countUsers(ranges[idx]);  if (logger.isDebugEnabled()) logger.debug(" " + idx + " has count " + counts[idx]);  totalUsers += counts[idx];  } 
 // now we can do the math...  double total = Double.valueOf(totalUsers); 
 for (int idx = 0; idx < 7; idx ++) {  percentages[idx] = 100.0 * (Double.valueOf(counts[idx]) / total); 
 if (logger.isDebugEnabled()) logger.debug("Percentage " + idx + " is " + percentages[idx] + "%");  } 
 timestamp = System.currentTimeMillis(); 
 return percentages;  } 
 /**  * countUsers: Counts the number of users for the given date range.  * @param range The date range to use for the query.  * @return long The number of users  */  protected long countUsers(DateRange range) {  if (logger.isDebugEnabled()) logger.debug("Looking for birthday from " + range.getStartDate() + " to " + range.getEndDate() + "."); 
 // create a new dynamic query for the Contact class (it has the birth dates).  DynamicQuery dq = DynamicQueryFactoryUtil.forClass(Contact.class, PortalClassLoaderUtil.getClassLoader()); 
 // restrict the count so the birthday falls between the start and end date.  dq.add(RestrictionsFactoryUtil.between("birthday", range.getStartDate(), range.getEndDate())); 
 long count = -1; 
 try {  // count the users that satisfy the query.  count = UserLocalServiceUtil.dynamicQueryCount(dq);  } catch (SystemException e) {  logger.error("Error getting user count: " + e.getMessage(), e);  } 
 if (logger.isDebugEnabled()) logger.debug("Found " + count + " users."); 
 return count;  } } 

This class will keep a cache of counts that will apply for one day.  After 24 hours the cached values are discarded and the queries will be performed again.

Probably not the best implementation, but I'm not shooting for realtime accuracy, just mostly accurate data suitable for a chart on say a dashboard page.

UsersChartsUI

Next comes the UserChartsUI class that implements the UI for the portlet.

package com.dnebinger.liferay.vaadin.userchart; 
import com.vaadin.annotations.Theme; import com.vaadin.server.VaadinRequest; import com.vaadin.ui.Alignment; import com.vaadin.ui.UI; import com.vaadin.ui.VerticalLayout; import com.dnebinger.vaadin.highcharts.UserPieChart; 
/**  * Created by dnebinger on 2/6/15.  */ @Theme("sample") public class UsersChartUI extends UI {  private VerticalLayout mainLayout; 
 private UserPieChart pieChart; 
 private static final UserAgeData userAgeData = new UserAgeData(); 
 @Override  protected void init(VaadinRequest vaadinRequest) {  // create the main vertical layout  mainLayout = new VerticalLayout(); 
 // give it a margin and space the internal components.  mainLayout.setMargin(true);  mainLayout.setSpacing(true);  mainLayout.setWidth("100%");  mainLayout.setHeight("100%"); 
 // set the layout as the content area.  setContent(mainLayout); 
 setStyleName("ui-users-chart"); 
 setHeight("400px"); 
 pieChart = new UserPieChart();  pieChart.setImmediate(true);  pieChart.setHeight("100%");  pieChart.setWidth("100%"); 
 mainLayout.addComponent(pieChart);  mainLayout.setComponentAlignment(pieChart, Alignment.TOP_CENTER); 
 double[] percentages = userAgeData.getPercentages(); 
 pieChart.updateUsers(percentages[0], percentages[1],percentages[2],percentages[3],percentages[4],percentages[5],percentages[6]);  } } 

Nothing special in the UI, it creates a UserPieChart instance and places it in the layout.

UserPieChart

The final class is the com.dnebinger.vaadin.highcharts.UserPieChart class.  This is the special integration class joining Vaadin 7 and HighCharts.

package com.dnebinger.vaadin.highcharts; 
import com.dnebinger.liferay.vaadin.userchart.UserAgeData; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.vaadin.annotations.JavaScript; 
/**  * Created by dnebinger on 2/6/15.  */ @JavaScript({"highcharts-custom.js", "highcharts-connector-nojq.js"}) public class UserPieChart extends AbstractHighChart {  private static final Log logger = LogFactoryUtil.getLog(UserAgeData.class);  private static final long serialVersionUID = 7380693815312826144L; 
 /**  * updateUsers: Updates the pie chart using the given percentages.  * @param age_0_10  * @param age_11_20  * @param age_21_30  * @param age_31_40  * @param age_41_50  * @param age_51_60  * @param age_60_plus  */  public void updateUsers(final double age_0_10, final double age_11_20, final double age_21_30, final double age_31_40, final double age_41_50, final double age_51_60, final double age_60_plus) { 
 StringBuilder sb = new StringBuilder("var options = { "); 
 sb.append("chart: {");  sb.append("plotBackgroundColor: null,");  sb.append("plotBorderWidth: null,");  sb.append("plotShadow: false");  sb.append("},");  sb.append("title: {");  sb.append("text: 'Age Breakdown for Registered Users'");  sb.append("},");  sb.append("plotOptions: {");  sb.append("pie: {");  sb.append("allowPointSelect: true,");  sb.append("cursor: 'pointer',");  sb.append("dataLabels: {");  sb.append("enabled: false");  sb.append("},");  sb.append("showInLegend: true");  sb.append("}");  sb.append("},");  sb.append("series: [{");  sb.append("type: 'pie',");  sb.append("name: 'Age Breakdown',");  sb.append("data: [");  sb.append("['Zero to 10', ").append(format(age_0_10)).append("],");  sb.append("['11 to 20', ").append(format(age_11_20)).append("],");  sb.append("['21 to 30', ").append(format(age_21_30)).append("],");  sb.append("['31 to 40', ").append(format(age_31_40)).append("],");  sb.append("['41 to 50', ").append(format(age_41_50)).append("],");  sb.append("['51 to 60', ").append(format(age_51_60)).append("],");  sb.append("['Over 60', ").append(format(age_60_plus)).append(']');  sb.append("]"); 
 sb.append("}]"); 
 // close the options  sb.append(" };"); 
 String chart = sb.toString(); 
 if (logger.isDebugEnabled()) logger.debug(chart); 
 setHcjs(chart);  } 
 protected String format(final double val) {  String s = String.format("%s", val); 
 int pos = s.indexOf('.'); 
 if (pos < 0) return s + ".0"; 
 double x = Double.valueOf(Double.valueOf((val * 10.0) + 0.5).longValue()) / 10.0; 
 return s.substring(0, pos+2);  } } 

This class requires the most explanation, so here we go.

The first thing is the @JavaScript annotation.  This is a Vaadin 7 annotation that is used to inject JavaScript files into the response stream for the portlet.  For this portlet we need the highcharts-custom.js file (this will be the one that you downloaded earlier) as well as a highcharts-connector-nojq.js file which we will add later.

The updateUsers() method takes the 7 percentage values and outputs JavaScript to create and populate an options object, this object will be used by the HighCharts library to render the chart.  I used a simple StringBuilder to build the JavaScript, but a better way would have been to use the org.json.JSONObject class to construct an object and output it as a string (that way you don't have to worry about syntax, quoting, etc.).

The last method, the format() method, is a simple method to output a double as a string to a single decimal place.  Nothing fancy, but it will work.

Adding the JavaScript Files

As previously stated, we need to add two javascript files to the project, but these do not go in the normal places.  Since we used the @JavaScript annotation in the UserPieChart class, we have to put them into the project's src/main/resources folder as the JavaScript files need to be in the class path.

Since our Java class is com.dnebinger.vaadin.highcharts.UserPieChart that has the @JavaScript annotation, our JavaScript files have to be in src/main/resources/com/dnebinger/vaadin/highcharts.  The HighCharts JavaScript file that we downloaded earlier must be copied to this directory named "highcharts-custom.js" (to match the value we used in the @JavaScript annotation).

The second javascript file we will create in this directory is "highcharts-connector-nojq.js".  The contents of this file should be:

window.com_dnebinger_vaadin_highcharts_UserPieChart = function () { 
 this.onStateChange = function () {  // read state  var domId = this.getState().domId;  var hcjs = this.getState().hcjs; 
 // evaluate highcharts JS which needs to define var "options"  eval(hcjs); 
 // update the renderTo to point at the dom element for the chart.  if (options.chart === undefined) {  options.chart = {renderTo: domId};  } else {  options.chart.renderTo = domId;  }   // create the new highcharts object to render the chart.  var chart1 = new Highcharts.Chart(options);  }; }; 

This is a connector JavaScript function to bridge Vaadin 7 and HighCharts.  Note the name of the function matches the package path and class for the function name, except the periods are replaced with underscores.

Since the current HighCharts Add On relies on jQuery, the connector function that comes with the Add On will just not work in our new implementation for the portal.  This function does not rely on jQuery and will ensure the correct stuff happens.

Conclusion

So far I've presented some code, but it's not all that much.  Some utility classes for date calculations, a wrapper class encapsulating the DynamicQuery access, our necessary UI class for the portlet and a UserPieChart class to encapsulate the HighCharts access.

The results of this code can be seen below:

User Ages Pie Chart

Had I taken a little time to create some users with suitable dates, I could make the chart take on any sort of view.  Since I'm using a stock Liferay bundle with my own extra user account, I have 3 users that must be broken down and displayed.

I think you must agree, however, that all in all this is an extremely simple portlet.  It should be easy to see how you could leverage a number of small Vaadin 7 portlets to create a dashboard page with quite a few graphs on it.

If I were asked to do something like that, I'd make a few changes:

If there were more than say 4 or 5 graphs on the page, I'd put the highcharts-custom.js JavaScript file into the theme.  I'd then remove it from the @JavaScript annotation.  The connector JavaScript would remain, but it is so small and is bound to the chart package/class anyway.

If there are more than two on the page but less than 5, I would at least put the highcharts-custom.js into the portlet javascript resources and pull in using the <header-portlet-javascript /> tag in liferay-portlet.xml.  That would at least allow the portal to import a single copy of the JavaScript rather than multiple copies.  Again it would be removed from the @JavaScript annotation, but the connector JavaScript would remain.

Note: Before anyone asks, I'm not going to be putting the code for this portlet online.  Without the HighCharts JavaScript, it's not really complete and I can't distribute the HighCharts JavaScript myself.  However, all of the code for the portlet is here in the blog and I've left nothing out.  If you have any problems reconstituting the portlet, feel free to ask.

Enjoy!