Sunday, June 17, 2007

Charting the web with Cewolf/JFreeChart - Producing Time Series plots

I recently had an opportunity to use the Cewolf 1.0 at work for some Time Series plots (a variant of the XY Chart, shown in the figure above, where the X-axis is for time values). This blog is about Cewolf and how to create time series plots with it.

Cewolf is a JSP tag library which uses JFreeChart for rendering the charts. It comes with a controller servlet which is used for interpreting the parameters passed through the JSP tag and accordingly generating the chart image in-memory (no files created on the file system of the server) and embeds the image as tag in the HTML output to the client response stream.

IMO, Cewolf/JFreeChart is the best free charting package for a web application required to draw charts and being developed in Java EE. It supports several different types of charts and one of them was the Time Series plots. Here is some code which can produce a simple time series plot (using cewolf).

1. To install Cewolf you just need to copy the jars from its lib/ path (which includes the JFreeChart jar too) to WEB-INF/lib of your web application.

2. We need to write a data producer which gets the data set (in {time, value} pairs for the time series plot). A typical time series data producer is given below:


//~--- non-JDK imports --------------------------------------------------------

import de.laures.cewolf.DatasetProduceException;
import de.laures.cewolf.DatasetProducer;

import org.jfree.data.time.Minute;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;

//~--- JDK imports ------------------------------------------------------------

import java.io.Serializable;

import java.util.Date;
import java.util.Map;

/**
* A sample data producer for the time series plot.
*/
public class MyDataProducer implements DatasetProducer, Serializable
{
public MyDataProducer()
{
}

public Object produceDataset(Map map) throws DatasetProduceException
{
/*
* To this time series collection we can add more than one time series
* where each time series will be represented by its own line on a
* plot.
*/
TimeSeriesCollection ts = new TimeSeriesCollection();

try {
String[] allSeries = { "series1", "series2" };

// Loop through all series and add the data to series and series to
// timeseries collection (ts).
for (int i = 0; i < allSeries.length; i++) {

// Get data for series from some kind of datasource
MyDataSet[] myDataSet = GetDataForSeries(allSeries[i]);
TimeSeries mySeries = new TimeSeries("My Data Series " + i, Minute.class);

// Add data to series
for (MyDataSet data : myDataSet) {
mySeries.add(new Minute(new Date(data.getTimestamp().getTime())), data.getYValue());
}

// Add the series to the collection
ts.addSeries(mySeries);
}
} catch (Exception e) {
e.printStackTrace();

throw new DatasetProduceException();
}

return ts;
}

public boolean hasExpired(Map map, Date date)
{
return false;
}

public String getProducerId()
{
return "My Data Producer";
}

private MyDataSet[] GetDataForSeries(String string)
{
// Get data from DB or some data source
// return an array of time/value pairs (for instance, as an array
// of MyDataSet instances.
}
}


MyDataSet class is:


import java.sql.Timestamp;

/** A sample time/value pair data. An array/list of this type will constitute
* the data set for the plot.
*/
public class MyDataSet
{
private Timestamp timestamp; // You can use other date/time types in Java SE here.
private double yValue;

public MyDataSet()
{
}

public Timestamp getTimestamp() {
return timestamp;
}

public void setTimestamp(Timestamp timestamp) {
this.timestamp = timestamp;
}

public double getYValue() {
return yValue;
}

public void setYValue(double yValue) {
this.yValue = yValue;
}
}


In the JSP page you include the chart now:


<jsp:usebean id="myPlotData" class="com.mycompany.MyDataProducer">

<cewolf:chart
id="MyChart"
type="timeseries"
title="My Plot Title"
xaxislabel="Time"
yaxislabel="My Data Value">
<cewolf:data>
<cewolf:producer id="myPlotData" usecache="false">
</cewolf:data>
</cewolf:chart>
<cewolf:img chartid="MyChart" renderer="/cewolf" width="1000" height="400">


Lastly, one needs to configure the CewolfServlet in the web.xml:
<servlet>
<servlet-name>CewolfServlet</servlet-name>
<servlet-class>de.laures.cewolf.CewolfRenderer</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>CewolfServlet</servlet-name>
<url-pattern>/cewolf/*</url-pattern>
</servlet-mapping>


The generated charts on tomcat does require one to increase the JVM heap size to at least 256MB from the default 64MB.

The pros of using Cewolf/JFreeChart in a Java EE web application:
1. Compared to the other open source packages, JFreeChart happens to be the best in terms of the look and feel of the plots and the ease of use of the API.
2. Cewolf contributes to the glory by making the JFreeChart available as JSP tag library. And as far as i know, there isn't any other better option for plotting in the open source Java world.

The duo of Cewolf/JFreeChart are lacking in a few important features:
1. AJAX support for real-time plots. So if we want the server to be able to asynchronously (or by the virtue of some background polling from client) refresh the chart in (near) real-time then its not something supported today in Cewolf/JFreeChart. If this feature is required then JViews Charts or Chart Director are the two commercial offerings that i know of that can do AJAX based live charts.
2. Zoom and Pan interactions are not easily supported by the API.

Sunday, June 10, 2007

Working with JMaki

I recently had an opportunity to use some of the JMaki UI components and it took me some googling to figure out how to pass data dynamically (which is what most of the time you will want and unfortunately all examples use some static data in the JSON format) to the UI components. JMaki's integration with Netbeans makes it really simple to have those nice Web UI components working for you in a jiffy (like grid, tree, menu, captcha, autocomplete etc). Though i am a big fan of DWR (having used the reverse ajax in DWR 2.0 for an event browser application to show events in real-time) for Ajax support in my work, i did like the Ajax-enabled UI Components that come with JMaki. Another nice thing about JMaki is, it provides a common data model for multiple implementations of a certain UI component. For example, you have a Yahoo UI Tree and a dojo toolkit tree component. Since JMaki provides the abstraction by keeping the data models same for both these tree components, so we have the option to switch between these implementations with almost no change to code.

Now going back to the point (the reason i am writing this post after all) ... the JMaki components accept data dynamically in JSON format and though one can create the JSON format string to pass as value attributes to the widgets, it becomes cumbersome for widgets like trees or grid to escape the quotes and construct the strings. To make our lives easy in constructing the JSON formatted dynamic data, JMaki comes bundled with org.json.* classes (JSONObject, JSONArray etc) using which one can create the data to pass in an elegant and maintainable way. You will need to convert the JSONObject or JSONArray types to their Object literal form using the following code which Greg Murray (JMaki project manager) released in reply to one post on JMaki users forum:


/**
* Converts a JSON Object to an Object Literal
*
*
* @param jo
* @param buff
*
* @return
*
* @throws JSONException
*/
public static String jsonToObjectLiteral(JSONObject jo, StringBuffer buff)
throws JSONException
{
if (buff == null) {
buff = new StringBuffer("{");
} else {
buff.append("{");
}

JSONArray names = jo.names();

for (int l = 0; (names != null) && (l < names.length()); l++) {
String key = names.getString(l);
String value = null;

if (jo.optJSONObject(key) != null) {
value = key + ":";
buff.append(value);
jsonToObjectLiteral(jo.optJSONObject(key), buff);
} else if (jo.optJSONArray(key) != null) {
value = key + ":";
buff.append(value);
jsonArrayToString(jo.optJSONArray(key), buff);
} else if (jo.optLong(key, -1) != -1) {
value = key + ":" + jo.get(key) + "";
buff.append(value);
} else if (jo.optDouble(key, -1) != -1) {
value = key + ":" + jo.get(key) + "";
buff.append(value);
} else if (jo.opt(key) != null) {
Object obj = jo.opt(key);

if (obj instanceof Boolean) {
value = key + ":" + jo.getBoolean(key) + "";
} else {
value = key + ":" + "'" + jo.get(key) + "'";
}

buff.append(value);
}

if (l < names.length() - 1) {
buff.append(",");
}
}

buff.append("}");

return buff.toString();
}

/**
* Converts a json array to string.
*
*
* @param ja
* @param buff
*
* @return
*
* @throws JSONException
*/
public static String jsonArrayToString(JSONArray ja, StringBuffer buff)
throws JSONException
{
if (buff == null) {
buff = new StringBuffer("[");
} else {
buff.append("[");
}

for (int key = 0; (ja != null) && (key < ja.length()); key++) {
String value = null;

if (ja.optJSONObject(key) != null) {
jsonToObjectLiteral(ja.optJSONObject(key), buff);
} else if (ja.optJSONArray(key) != null) {
jsonArrayToString(ja.optJSONArray(key), buff);
} else if (ja.optLong(key, -1) != -1) {
value = ja.get(key) + "";
buff.append(value);
} else if (ja.optDouble(key, -1) != -1) {
value = ja.get(key) + "";
buff.append(value);
} else if (ja.optBoolean(key)) {
value = ja.getBoolean(key) + "";
buff.append(value);
} else if (ja.opt(key) != null) {
Object obj = ja.opt(key);

if (obj instanceof Boolean) {
value = ja.getBoolean(key) + "";
} else {
value = "'" + ja.get(key) + "'";
}

buff.append(value);
}

if (key < ja.length() - 1) {
buff.append(",");
}
}

buff.append("]");

return buff.toString();
}


So, after you have your dynamic data put in JSONObject or JSONArray, you can invoke the corresponding conversion method stated above to get the String form of your JSON data ready to be passed to the component.

For instance, in your tree builder code, you will need to do the following (the example is from this post where the solution was posted by Greg Murray):


public static JSONObject buildTreeData(AuthorizedTeams ateams)
throws JSONException {

JSONObject retValue = new JSONObject();
JSONObject root = new JSONObject();
root.put ("title", "Organizations");
root.put ("expanded", true);
JSONArray data = new JSONArray();

Team[] teams = ateams.getTeams();

for (int i=0; i<teams.length; i++) {
JSONObject teamObj = new JSONObject();
teamObj.put("title", teams[i].getTeamName());
teamObj.put("expanded", true);

JSONArray children = new JSONArray();

User[] teamUsers = teams[i].getMembers();
for (int j=0; j<teamUsers.length; j++) {
JSONObject childObj = new JSONObject();
childObj.put("title",teamUsers [j].getUserName());
children.put(childObj);
}
teamObj.put("children", children);
data.put(teamObj);
}
root.put ("children", data);
retValue.put ("root", root);

return jsonToObjectLiteral(retValue, new StringBuffer());
}

Here is the JSP snippet:

<jsp:useBean id="teams"
class="com.myapp.assignment.AuthorizedTeams"
scope="request"/>
<a:widget name="dojo.tree" value="${teams.teamsData}">