Javascript: Mixing Ext’s Grid with JQuery’s Flot

maanantaina, helmikuuta 11, 2008

Data visualization through charts and graphs is incredibly common and yet Ext doesn’t have a readily apparent charting library! JQuery has a great little charting library called Flot and getting it to work within an Ext application turned out to be pretty easy. I’ll show how I integrated the two and the problems I ran into. I’ll also go over some common issues in choosing a charting library.
Charts, whether static or dynamic, are a common requirement in almost every application with a grid. I say almost because some users could care less for a chart, they just want the data and can visualize the data just fine on their own. Many users can’t imagine the data on their own and need some kind of chart sitting near any table or grid. The volume of data in a grid may also be so large that a user simply can’t imagine the breakdown without some automated visualization.
I mentioned two kinds of charts and they are worth discussing briefly. Static charts are usually non- or minimally interactive images generated on a server and displayed on the browser. They often have image maps that let a user click on a line or a point and drill-down into the data. Then the browser loads the next chart image and the user can continue drilling down. The refresh rate is obviously slow, because there is a roundtrip between the browser and the server when redrawing the chart, and the interaction is limited.
Dynamic charts are usually flash or Javascript based and are excellent for displaying fast moving data. These kinds of charts are usually more interactive: allowing for zoom, filtering and rapid updates. They can provide a better user experience depending upon on the volume of data to draw.
Choosing one method over another is dependent on product requirements and volume of data. Consider that you want the ability to cut-and-paste a chart from a web application into an email. Static images work great, but javascript and flash charts usually need some kind of external capture utility like SnagIt(PC) or Grab (Mac).
Images work very well when you have huge amounts of data to plot. A scatter plot with 5000 points runs well on a server. Trying that with a javascript control incurs both the rather large download of the data and the long processing time, which can sometimes stretch long enough for the browser to popup the dialog asking the user if he wants to continue running the script. The image again can be cached on the server, so the creation time is incurred only when the data changes, which is hopefully only once every couple of minutes. Images with image maps still have a long download though.
Javascript and Flash charts can interact seamlessly with a Javascript application. The chart’s look and feel is often far more polished and its responsiveness is often surprising to users.
Again, product requirements and volume of data are the most important drivers for a technology than the technology itself. It often happens that a product manager will want a flash chart when a static image chart is actually more effective. It also happens that someone wants a static image chart to support a rarely used requirement rather than a dynamic chart to show rapidly changing data, which is a more often used feature.
I’ve yammered on about charting technologies long enough. I started by saying that I needed to integrate a Javascript chart library into an Ext based grid application. I’ve taken a previous example of the EditorGridPanel and extended it with charts and a few other minor embellishments. I went along with JQuery’sFlot because I only needed bar charts, but it also supports line and scatter plots too.
Ext has an unusual feature that allows you to swap out its default base library and replace it with one that acts as a facade on other libraries, like JQuery. I am not going to take advantage of this feature for two reasons. First, Flot uses a more recent version of JQuery than what is distributed with Ext. Second, I am more interested in seeing if Ext will continue to operate with a new library messing around with the DOM. The second reason is especially important if we later want to use a library that supports pie charts or another chart not supported by flot.
The integration process is very easy and didn’t run into any problems that couldn’t be worked around. Flot needs only a few things to build a chart. It obviously needs data which I will supply via a Store. We need some event support to update the charts when data changes. We need a container to display the charts within the Ext application. Finally, it would be really snazzy if the charts updated as the data changes. Let’s look at the code to see how these were accomplished. The following description will refer to the red numbered comments. Also note that the code is just an example and not meant to be used in production.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Ext EditorGridPanel with JQuery’s Flot Example</title>
<link rel="stylesheet" type="text/css"
href="./ext-2.0.1/resources/css/ext-all.css" />
<script type="text/javascript" src="./ext-2.0.1/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="./ext-2.0.1/ext-all-debug.js"></script>
<script type="text/javascript" src="./flot/jquery.js"></script>
<script type="text/javascript" src="./flot/jquery.flot.js"></script>
<script>
var initComplete = false;

function main() {
// Setup the store and its data
var sampleData = [
[ 'Bob', 20, 13.45 ],
[ 'Bill', 40, 12.92],
[ 'Mike', 45, 23.02]
];

var store = new Ext.data.SimpleStore({
fields : [
{ name: 'name', type: 'string' },
{ name: 'hours', type: 'int' },
{ name: 'rate', type: 'float' },
{ name: 'cost', type: 'float' }
],
data: sampleData
});

// Initialize the grid control
var simpleGrid = new Ext.grid.EditorGridPanel({
store: store,
columns: [{
header: 'Name', // Field cannot be edited.
width: 160,
sortable: false,
dataIndex: 'name'
},
{
header: 'Hours', // Field can be edited
width: 75,
sortable: false,
dataIndex: 'hours',
editor: new Ext.form.NumberField({
allowBlank: false,
allowDecimals: false
})
},
{
header: 'Rate', // Field can be edited
width: 75,
sortable: false,
dataIndex: 'rate',
editor: new Ext.form.NumberField({
allowBlank: false,
allowDecimals: true
})
},
{
header: 'Cost', // Calculated field
width: 75,
sortable: false,
renderer: formatCost,
dataIndex: 'cost'
}
],
clicksToEdit: 1,
stripeRows: true,
height: 100,
enableHdMenu: false,
region: 'south'
});
var commonChartStyle = {"margin": "10px 10px 10px 10px"};
// 1: Set up the containers for the charts
var rateContainer = new Ext.Panel({
height: 200,
width: 200,
style: commonChartStyle,
id: 'rateChartContainer'
});
var hoursContainer = new Ext.Panel({
height: 200,
width: 200,
style: commonChartStyle,
id: 'hoursChartContainer'
});
var costContainer = new Ext.Panel({
height: 200,
width: 200,
style: commonChartStyle,
id: 'costChartContainer'
});
var viewPort = new Ext.Panel({
title: 'Chart-Grid Example',
frame: true,
layout: 'border',
renderTo: Ext.getBody(),
height: 400,
width: 674,
items: [{
  region: 'center',
border: true,
bodyStyle: 'background-color: white; border: 1px solid #99BBE8',
layout: 'column',
items: [{
title: 'Hours',
width: 220,
items: hoursContainer
},{
title: 'Rate',
width: 220,
items: rateContainer
},{
title: 'Cost',
width: 220,
items: costContainer
}
]
},
simpleGrid]
});
drawCharts(store);
initComplete = true;
}

function drawCharts(store) {
drawChart(store, 'rate', 'rateChartContainer');
drawChart(store, 'hours', 'hoursChartContainer');
drawChart(store, 'cost', 'costChartContainer');
}

function getData(store, nameColumn, dataColumn) {
var dataResults = new Array();
var tickResults = new Array();
// 2: get the chart data
for( var recordIndex = 0; recordIndex < store.getCount(); recordIndex++ ) {
var record = store.getAt(recordIndex);
var tmpData = [(recordIndex+1)*2, record.get(dataColumn)];
var series = {
bars: { show: true },
label: record.get(nameColumn),
data: [ tmpData, tmpData ], // workaround
color: recordIndex
}
dataResults.push( series );
tickResults.push([ ((recordIndex+1)*2)+1, record.get(nameColumn) ]);
}
return {
data:dataResults,
ticks:tickResults
};
}

function drawChart(store, column, chart) {
var chartInfo = getData(store, 'name', column);
// 3: draw the chart in the container
$.plot($("#"+chart),
chartInfo.data,
{
xaxis: {
autoscaleMargin: .25,
ticks: chartInfo.ticks
}
}
);
}

// Formatting function for the cost column
function formatCost(value, metadata, record, rowIndex, colIndex, store) {
var results = getCost(record);
record.set( 'cost', results);
if ( initComplete ) {
// 4: redraw the charts when the data changes
drawCharts(store);
}
return Ext.util.Format.usMoney(results);
}

// Calculates the cost
function getCost(record) {
var hours = record.get('hours');
var rate = record.get('rate');
var results = hours * rate;
return results;
}

</script>
<style>
p {
margin:5px;
}
</style>
</head>
<body style="margin: 10px 10px;">
<script>
Ext.onReady( main );
</script>
</body>
</html>
First, we create three nearly identical Ext.Panel(1) objects to contain the chart. They all have hard coded sizes and an explicit id field. Flot will calculate the size of the chart based upon the size of the container. If you let Ext automatically set the height or width, Flot may see the value as being 0 and make a very small chart. The user can resize the panel, but you must ensure that the height and width are valid.
The id field is necessary so that we can point JQuery to the right DOM object to modify. You can use this technique with other charting libraries too. Ext will generate a div with the id you specify. Once generated, it is easy to point any other compatible library at the div and generate whatever you want.
Second, we load the data for the chart from the Store(2). Flot’s data series has the format of [ [x axis value, y axis value]] . A line chart would have multiple x-y value pairs.
We will also replace the normal tick values with a person’s name, which is done by substituting an axis value with a label. Each axis value can represent a range from N to N+1. The 0 position of the X axis is flush to the Y axis. Having a label on the 0 location makes the label look strange and somewhat unreadable.The goal of the tmpData assignment line is to move the X axis value away from the 0 position. 
The code then creates a series object that sets the chart type to bar, the label for the series, the color and the data. The data line is really odd because there is a bug in the Flot library for bar charts. Flot requires more than 1 data point or it doesn’t render the series. The workaround listed in the bug report is to have two identical values in the series. That’s why tmpData is used twice.
Another interesting aspect of Flot is that each series can specify a different chart type. You can mix line, scatter and bar charts in a single chart. Generally it is unwise to do so as it makes for a cool looking chart that doesn’t necessarily make the data more readable. Google Edward Tufte and you will find plenty of documentation on good charting techniques.
The last interesting line deals with the ticks. The tick values are using a different index than what was used earlier. This was done so that the label would be on the trailing edge of the bar rather than the leading edge and looks marginally better.
Third, we render the chart(3). The plot method gets called with three parameters: the id for the container, an array of series data, and the chart options. The container id reference finds the DOM object and adds a couple ofcanvas and div tags to the Panel. Flot uses the array of series data to draw the chart. Finally, the options parameter modifies the X axis to change margins to be more readable and replaces the ticks with the values mentioned earlier.
Fourth, we modify the formatter to update the charts when the grid changes(4). The effect is that a user can modify either the rate or hours columns and the charts immediately update.
You will see a few odd sections of code wrapped in if(initComplete)statements. JQuery cannot go to work unless all the Ext objects are rendered, which may not occur until later. The if statement ensures that the UI is fully rendered before trying to run Flot code.
The goal of this example was to show that it is possible to integrate Ext with other major Javascript frameworks to supplement Ext’s features. In this case we added a chart to an existing project. The selection of the chart library was almost arbitrary, though I tried a few others and thought that Flot made a really nice looking bar chart. Now you can go off and try mixing libraries using the techniques shown here.

You Might Also Like

0 comments