Showing posts with label JFreeChart. Show all posts
Showing posts with label JFreeChart. Show all posts

Monday, December 15, 2008

Generating a Climograph with JFreeChart

A climograph can be easily generated with Excel as documented in Creating a Climograph in Excel and Directions for Using Excel to Make a Colorful Climograph. While it is a little more work to generate a climograph with JFreeChart, it is still pretty straightforward as demonstrated in this blog post. The advantage, of course, is that one can then easily generate a climograph directly from Java.

As its name suggests, the climograph is a useful graphical representation of climate data. Specifically, it displays precipitation and temperature data on the same chart. Typically, precipitation data is displayed as a bar chart and is overlaid with temperature data portrayed as a line chart.

The article Visualize Your Oracle Database Data with JFreeChart demonstrates how to build several different types of charts, but the article does not demonstrate building a climograph. In fact, most of the charts discussed in the article are charts generated directly from the ChartFactory class. A secondary motivation for this blog post is to demonstrate this additional chart type and some of the features used in JFreeChart to build this "custom" chart type.

I'll focus on some snippets of the code for generating a climograph first and then list the code in its entirety at the end of this post. The first code snippet shown is the method buildMainChartWithPrecipitation. The method uses ChartFactory.createBarChart to generate the bar chart representing precipitation. So far, this is the same old bar chart generation code one would see for generating any bar chart. Adding the line chart later will make this more interesting.

buildMainChartWithPrecipitation


/**
* Construct the initial chart with focus on the precipitation portion.
*
* @param precipitationDataset Precipitation dataset.
* @return First version of chart with precipitation data rendered on the
* chart.
*/
public JFreeChart buildMainChartWithPrecipitation(
final CategoryDataset precipitationDataset)
{
final JFreeChart chart =
ChartFactory.createBarChart(
this.chartTitle,
CHART_CATEGORY_AXIS_LABEL,
chartPrecipitationValueAxisLabel,
precipitationDataset,
CHART_ORIENTATION,
true, // legend required?
false, // tool tips?
false); // URLs generated?
customizeChart(chart.getCategoryPlot());
return chart;
}


The complete code listing at the end of this post will show the setting of the values used in this code such as title, orientation, and axis label. The precipitationDataset is prepared by a method that accepts an array of twelve numbers representing precipitation for each of the twelve months and places these values in a CategoryDataset.

The customizeChart method is needed to put the names of the months at a 45-degree angle because they won't fit on the graph in their full form otherwise.

With the bar chart representing the precipitation measurements, we now need to add a line chart representing temperature measurements to this chart to get the climograph. The following method, addTemperatureMeasurementsToChart,
adds the line chart representation of the measured temperatures to the previously generated bar chart representation of measured precipitation.

addTemperatureMeasurementsToChart


/**
* Add temperature-oriented line graph to existing chart.
*
* @param originalChart Chart to which line graph is to be added.
* @param temperatureDataset Dataset for the line graph.
*/
public void addTemperatureMeasurementsToChart(
final JFreeChart originalChart,
final CategoryDataset temperatureDataset)
{
final CategoryPlot plot = originalChart.getCategoryPlot();
final ValueAxis temperatureAxis =
new NumberAxis(this.chartTemperatureValueAxisLabel);
plot.setDataset(1, temperatureDataset);
plot.setRenderer(1, new LineAndShapeRenderer(true, true));
plot.setRangeAxis(1, temperatureAxis);
plot.mapDatasetToRangeAxis(1, 1);
plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
}


The code above adds the line graph representation of temperature data to the bar chart representation of precipitation data by accessing the original bar chart's category plot and manipulating that. Specifically, the line chart is added at the second index value (1 because it is a zero-based index). The LineAndShapeRenderer is constructed with two true valuesto indicate that both the lines and the shapes at each data point should be displayed. The DatasetRenderingOrder is also set as FORWARD to ensure that the line chart will be displayed on top of the bar chart. The temperature dataset is a CategoryDataset that is very similar to the one prepared for precipitation and its preparation is also shown in the full code listing.

The full code listing of the ClimographGenerator class is shown next:


package dustin.climograph;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Logger;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;

/**
* Example of generation of climograph with JFreeChart.
*
* @author Dustin
*/
public class ClimographGenerator
{
private final static Logger LOGGER =
Logger.getLogger(ClimographGenerator.class.getCanonicalName());

/** Degree symbol Unicode representation. */
private final static String DEGREE_SYMBOL = "\u00B0";

/** Title of generated chart. */
private String chartTitle;

/** Labels used for chart's categories (months). */
private final static String[] MONTHS_CATEGORIES =
{ "January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December" };

/** Precipitation series label. */
private final static String PRECIPITATION_SERIES_LABEL = "Precipitation";

/** Temperature series label. */
private final static String TEMPERATURE_SERIES_LABEL = "Temperature";

/** Category Axis Label (months). */
private final static String CHART_CATEGORY_AXIS_LABEL = "Months";

/** Value Axis Label (precipitation). */
private String chartPrecipitationValueAxisLabel;

/** Value Axis Label (temperature). */
private String chartTemperatureValueAxisLabel;

/** Orientation of generated chart. */
private final static PlotOrientation CHART_ORIENTATION = PlotOrientation.VERTICAL;

/** No-arguments constructor not intended for public consumption. */
private ClimographGenerator() {}

/**
* Preferred approach for obtaining an instance of me.
*
* @param chartTitle Title to be used on generated chart.
* @param precipitationUnit Units used for precipitation.
* @param temperatureUnit Units used for temperature.
* @return An instance of me.
*/
public static ClimographGenerator newInstance(
final String chartTitle,
final PrecipitationUnit precipitationUnit,
final TemperatureUnit temperatureUnit)
{
final ClimographGenerator instance = new ClimographGenerator();
instance.chartTitle = chartTitle;
instance.setPrecipitationValueAxisLabel(precipitationUnit);
instance.setTemperatureValueAxisLabel(temperatureUnit);
return instance;
}

/**
* Set preciptation value axis label.
*
* @param precipitationUnit Units being used for precipitation measurements.
*/
private void setPrecipitationValueAxisLabel(
final PrecipitationUnit precipitationUnit)
{
this.chartPrecipitationValueAxisLabel =
"Precipitation (" + precipitationUnit.getChartDisplay() + ")";
}

/**
* Set the temperature value axis label.
*
* @param temperatureUnit Units being used for temperature measurements.
*/
private void setTemperatureValueAxisLabel(
final TemperatureUnit temperatureUnit)
{
this.chartTemperatureValueAxisLabel =
"Temperature (" + DEGREE_SYMBOL + temperatureUnit.getChartDisplay() + ")";
}

/**
* Construct dataset representing the precipitation.
*
* @param precipitationValues Numeric precipitation values, one for each of
* the twelve months.
* @return Dataset representating the precipitation.
* @throws IllegalArgumentException Thrown if more than 12 or fewer than 12
* values are provided for precipitation measurements.
*/
public CategoryDataset buildPrecipitationDataset(
final Number[] precipitationValues)
{
final DefaultCategoryDataset dataset = new DefaultCategoryDataset();
final int numberValues = precipitationValues.length;
if ( numberValues != 12)
{
throw new IllegalArgumentException(
"Twelve precipitation values need to be supplied, but only "
+ numberValues + " values were provided.");
}
for (int index=0; index < numberValues; index++ )
{
dataset.addValue(
precipitationValues[index],
PRECIPITATION_SERIES_LABEL,
MONTHS_CATEGORIES[index]);
}
return dataset;
}

/**
* Construct dataset representing the temperature.
*
* @param temperatureValues temperature values, one for each month.
* @return Dataset representing the temperature.
* @throws IllegalArgumentException Thrown if more than 12 or fewer than 12
* values are provided for temperature measurements.
*/
public CategoryDataset buildTemperatureDataset(
final Number[] temperatureValues)
{
final DefaultCategoryDataset dataset = new DefaultCategoryDataset();
final int numberValues = temperatureValues.length;
if ( numberValues != 12)
{
throw new IllegalArgumentException(
"Twelve temperature values need to be supplied, but only "
+ numberValues + " values were provided.");
}
for (int index=0; index < numberValues; index++ )
{
dataset.addValue(
temperatureValues[index],
TEMPERATURE_SERIES_LABEL,
MONTHS_CATEGORIES[index]);
}
return dataset;
}

/**
* Construct the initial chart with focus on the precipitation portion.
*
* @param precipitationDataset Precipitation dataset.
* @return First version of chart with precipitation data rendered on the
* chart.
*/
public JFreeChart buildMainChartWithPrecipitation(
final CategoryDataset precipitationDataset)
{
final JFreeChart chart =
ChartFactory.createBarChart(
this.chartTitle,
CHART_CATEGORY_AXIS_LABEL,
chartPrecipitationValueAxisLabel,
precipitationDataset,
CHART_ORIENTATION,
true, // legend required?
false, // tool tips?
false); // URLs generated?
customizeChart(chart.getCategoryPlot());
return chart;
}

/**
* Add temperature-oriented line graph to existing chart.
*
* @param originalChart Chart to which line graph is to be added.
* @param temperatureDataset Dataset for the line graph.
*/
public void addTemperatureMeasurementsToChart(
final JFreeChart originalChart,
final CategoryDataset temperatureDataset)
{
final CategoryPlot plot = originalChart.getCategoryPlot();
final ValueAxis temperatureAxis =
new NumberAxis(this.chartTemperatureValueAxisLabel);
plot.setDataset(1, temperatureDataset);
plot.setRenderer(1, new LineAndShapeRenderer(true, true));
plot.setRangeAxis(1, temperatureAxis);
plot.mapDatasetToRangeAxis(1, 1);
plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
}

/**
* Customize the generated chart.
*
* @param plot Plot associated with generated chart.
*/
public void customizeChart(final CategoryPlot plot)
{
final CategoryAxis monthAxis = plot.getDomainAxis();
monthAxis.setCategoryLabelPositions(
CategoryLabelPositions.DOWN_45);
}

/**
* Write .png file based on provided JFreeChart.
*
* @param chart JFreeChart.
* @param fileName Name of file to which JFreeChart will be written..
* @param width Width of generated image.
* @param height Height of generated image.
*/
public void writePngBasedOnChart(final JFreeChart chart,
final String fileName,
final int width,
final int height )
{
try
{
ChartUtilities.writeChartAsPNG(new FileOutputStream(fileName),
chart,
width, height);
}
catch (IOException ioEx)
{
LOGGER.severe("Error writing PNG file " + fileName);
}
}

/**
* Main chart-building executable.
*
* @param arguments The command line arguments; none expected.
*/
public static void main(final String[] arguments)
{
final ClimographGenerator me =
newInstance(
"Climograph for Fantasy Land",
PrecipitationUnit.INCHES,
TemperatureUnit.FARENHEIT);
final CategoryDataset precipitationDataset = me.buildPrecipitationDataset(
new Number[]{35, 30, 50, 40, 40, 30, 25, 15, 35, 40, 45, 50});
final JFreeChart chart = me.buildMainChartWithPrecipitation(
precipitationDataset);
final CategoryDataset temperatureDataset = me.buildTemperatureDataset(
new Number[]{30, 25, 45, 60, 70, 85, 90, 95, 75, 60, 40, 35});
me.addTemperatureMeasurementsToChart(chart, temperatureDataset);
me.writePngBasedOnChart(chart, "C:\\example.png", 600, 400);
}
}


The example above is current written to write a PNG image containing the generated climograph to the specified file name. Most of the class implements in a relatively generic way the generation of a climograph. Most of the hard-coded values and choices are isolated to the main function. This is intentional so that the class could be used in other contexts such as being called by a Swing application, being called by a servlet application, being called by a Flex or OpenLaszlo application, being called by a command-line tool or script, etc.

For completeness, I include the definitions of the two enums used in the above code next. These define the units for precipitation and temperature respectively.

PrecipitationUnit


package dustin.climograph;

/**
* Simple enum for representing the units used with precipitation for
* generating a climograph with JFreeChart.
*
* @author Dustin
*/
public enum PrecipitationUnit
{
CENTIMETERS("cm"),
INCHES("in"),
METERS("m"),
FEET("ft");

/** String to be displayed on generated chart. */
private String chartDisplayableString;

/**
* Constructor accepting string to be displayed on chart for the type of unit.
*
* @param newChartDisplayableString String to be displayed on chart for the
* unit type.
*/
PrecipitationUnit(final String newChartDisplayableString)
{
this.chartDisplayableString = newChartDisplayableString;
}

/**
* Provide my string representation of the unit to appear on the generated
* chart for this unit.
*
* @return String representation on chart for this unit type.
*/
public String getChartDisplay()
{
return this.chartDisplayableString;
}
}



TemperatureUnit


package dustin.climograph;

/**
* Representation of temperature units used in generation of Climograph with
* JFreeChart.
*
* @author Dustin
*/
public enum TemperatureUnit
{
CELSIUS("C"),
FARENHEIT("F");

private String chartDisplayableString;

/**
* Constructor accepting String indicating units being used for temperature
* measurement and intended for display on generated Climograph chart.
*
* @param newDisplayableString String to indicate units used on climograph
* for temperature.
*/
TemperatureUnit(final String newDisplayableString)
{
this.chartDisplayableString = newDisplayableString;
}

/**
* Provide String indicating units of measure used for temperatures shown
* in the JFreeChart-generated climograph.
*
* @return String representation of temperature units.
*/
public String getChartDisplay()
{
return this.chartDisplayableString;
}
}



When the above code is executed, the PNG image that appears next is generated. Click on the image to see a larger version of it.




Conclusion

It is relatively easy to generate a climograph from Java with JFreeChart. It is not as easy as with Excel, but is still pretty easy relative to other Java-based methods. With the code for the ClimographGenerator class shown above now available, it is a straightforward task to make some small modifications that will make the class even more modular and reusable for generic climograph generation. See the article Visualize Your Oracle Database Data with JFreeChart for a more detailed introduction to JFreeChart.

Monday, August 25, 2008

Flex Charts with Google Charts and Eastwood Charts

Flex provides a rich, sophisticated set of dynamic charts, but a license fee is required (via purchase of FlexBuilder Professional) to use these Flex charts for most realistic production situations. There are several other charting options including Open Flash Chart (runs as its own SWF and reads JSON data files to generate charts), amCharts (not open source and uses ActionScript 2 so cannot be used directly in Flex), Fusion Charts Free (sends SWF and data files from server to browser), Google Charts, and JFreeChart/Eastwood Charts. In this blog entry, I'll briefly look at using Google Charts and Eastwood Charts 1.1.0 with Flex because I believe these are the easiest to use in a Flex application.

Because the Eastwood Chart Servlet implements the most of the APIs of Google Charts, my example will be able, in most cases, to call either charting provider based on user selection. There are some extensions in Eastwood beyond the Google Chart API and the Google Chart API has some features still not implemented in Eastwood, but most of the common functionality is commonly implemented in both charting providers.

The concept behind Google Chart API (and hence applies to Eastwood Chart Servlet as well) involves providing data for chart generation to the chart provider via HTTP URL parameters and getting an image (PNG) as a response.

While URLs constructed to request chart generation from Google Charts and Eastwood Charts are almost always the same (because Eastwood is implemented to the Google Charts API), there are differences between the two implementations of these APIs. Using Google Charts is really easy because no work needs to be done on the server-side. All the developer needs to do is construct the appropriate URL conforming to the Google Charts API and invoke that to get the returned image. With Eastwood Charts, the developer needs to install the Eastwood-provided WAR file on his or her favorite Java EE container. It is important to note that this is all that must be done. The WAR files does not need to be tweaked or manually unzipped, but simply needs to be deployed by dropping it in the appropriate container directory or else through the server's administration tool.

The Eastwood Developer Manual that is included with the Eastwood download contains a list of several bullets outlining some advantages of the Eastwood Charts implementation of the Google Charts API. Most of these advantages stem ability to run the charting server on one's own network. Advantages of running the charting provider on one's own machine is include the ability to keep potentially sensitive data within one's own environment and the ability to run the chart generation in environments in which Internet connectivity is not available.

The following MXML code is a simple but complete example that accesses both Google Charts and Eastwood Charts via the same URL. I intentionally included 3D bar chart examples to demonstrate an extension provided by Eastwood Charts, but not by the Google Charts. While I don't show it here, Google Charts similarly provide some features not currently supported in Eastwood Charts (such as Maps).

Here is the source code for the Flex application taking advantage of both Google Charts and Eastwood Charts:

MXML Code for Application Using Eastwood Charts and Google Charts

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
width="900" height="900">
<mx:Script>
<![CDATA[
import mx.events.FlexEvent;

/**
* Generate chart using Eastwood Chart Servlet or Google Charts using
* submitted values.
*/
private function generateChart(event:FlexEvent):void
{
chartDisplayPanel.visible = true;
chartDisplayPanel.title = chartType.selectedValue.toString();
const url:String = buildUrl(chartType.selection.id);
chartUrlText.text = url;
generatedChart.source = url;
}

/**
* Determine maximum value entered for balances.
*
* @return Maximum balance from provided input balances.
*/
private function findMaximumDataValue():Number
{
const maximumBalance:Number =
Math.max(
Number(savingsInput.text),
Number(checkingInput.text),
Number(moneyMarketInput.text),
Number(taxableStocksInput.text),
Number(taxableBondsInput.text),
Number(iraInput.text),
Number(pensionInput.text) );
return maximumBalance;
}

/**
* Return the appropriate chart provider URL domain.
*
* @return URL domain for chart provider.
*/
private function findChartProvider():String
{
var chartProviderUrl:String;
if ( chartProvider.selection.id == "google" )
{
chartProviderUrl = "http://chart.apis.google.com";
}
else
{
chartProviderUrl = "http://localhost:8080/eastwood-1.1.0";
}
return chartProviderUrl;
}

/**
* Construct labels portion of chart generation URL.
*
* @return Labels portion of chart generation URL.
*/
private function constructLabels():String
{
const labelsStr:String =
"Savings|Checking|Money Market|Taxable Stocks|"
+ "Taxable Bonds|IRA|Pension";
return labelsStr;
}

/**
* Construct URL for accessing Eastwood-generated or Google-generated chart.
*/
private function buildUrl(chartType:String):String
{
const maximumBalance:Number = findMaximumDataValue();
const chartProviderDomainUrl:String = findChartProvider();
const urlString:String =
chartProviderDomainUrl + "/chart?cht="
+ chartType // type of chart
+ "&chs=400x400" // chart size: width x height
+ "&chd=t:" // build up the data over next several lines
+ savingsInput.text + ","
+ checkingInput.text + ","
+ moneyMarketInput.text + ","
+ taxableStocksInput.text + ","
+ taxableBondsInput.text + ","
+ iraInput.text + ","
+ pensionInput.text
+ "&chds=0," + Math.round(maximumBalance) // data scaling
+ "&chl=" + constructLabels();
return urlString;
}
]]>
</mx:Script>

<mx:HBox id="mainHorizontalBox" width="95%">
<mx:Panel id="inputPanel" title="Flex and Eastwood Charts">
<mx:Form>
<mx:FormHeading label="Investments Distribution" />
<mx:FormItem label="Savings ($)">
<mx:TextInput id="savingsInput" />
</mx:FormItem>
<mx:FormItem label="Checking ($)">
<mx:TextInput id="checkingInput" />
</mx:FormItem>
<mx:FormItem label="Money Market ($)">
<mx:TextInput id="moneyMarketInput" />
</mx:FormItem>
<mx:FormItem label="Taxable Stocks ($)">
<mx:TextInput id="taxableStocksInput" />
</mx:FormItem>
<mx:FormItem label="Taxable Bonds ($)">
<mx:TextInput id="taxableBondsInput" />
</mx:FormItem>
<mx:FormItem label="IRA ($)">
<mx:TextInput id="iraInput" />
</mx:FormItem>
<mx:FormItem label="Pension ($)">
<mx:TextInput id="pensionInput" />
</mx:FormItem>
<mx:FormItem label="Chart Type" id="chartTypeFormItem">
<mx:RadioButtonGroup id="chartType" />
<mx:RadioButton groupName="chartType" id="p"
label="Pie"
value="Investments Pie Chart" />
<mx:RadioButton groupName="chartType" id="p3"
label="3D Pie"
value="Investments 3D Pie Chart" />
<mx:RadioButton groupName="chartType" id="bvs"
label="Vertical Bar"
value="Investments Vertical Bar" />
<mx:RadioButton groupName="chartType" id="bvs3"
label="Vertical 3D Bar"
value="Investments Vertical 3D Bar"
selected="true" />
<mx:RadioButton groupName="chartType" id="bhs"
label="Horizontal Bar"
value="Investments Horizontal Bar" />
<mx:RadioButton groupName="chartType" id="bhs3"
label="Horizontal 3D Bar"
value="Investments Horizontal 3D Bar" />
</mx:FormItem>
<mx:FormItem label="Chart Provider"
id="chartProviderFormItem">
<mx:RadioButtonGroup id="chartProvider" />
<mx:RadioButton groupName="chartProvider" id="google"
label="Google Charts API"
value="Google Charts API" />
<mx:RadioButton groupName="chartProvider" id="eastwood"
label="Eastwood Chart Servlet"
value="Eastwood Chart Servlet"
selected="true" />
</mx:FormItem>
<mx:Button id="submitButton" label="Generate Chart"
buttonDown="generateChart(event);" />
</mx:Form>
</mx:Panel>
<mx:Panel id="chartDisplayPanel" height="{inputPanel.height}"
visible="false">
<mx:Image id="generatedChart" />
</mx:Panel>
</mx:HBox>
<mx:Panel id="summaryPanel" width="{mainHorizontalBox.width}"
title="URL Being Used for Chart Generation">
<mx:TextArea id="chartUrlText" width="{summaryPanel.width}"
height="75" />
</mx:Panel>

</mx:Application>


The above example provides a radio button group for the user to select which chart provider (Google Charts or Eastwood Charts) should be used. Another radio button group allows the user to select which chart type should be generated. The text fields allow for dynamic data values to be supplied that are used in the chart generation. The sample code takes these input values, the user's selection for chart type, and the user's selection for chart provider and builds a URL from these data points that meets the Google Charts API.

The following screen snapshots show charts generated from both Google Charts and Eastwood Charts (click on the images to see larger versions).

Eastwood Charts-Generated 3D Pie Chart



Google Charts-Generated 3D Pie Chart



Eastwood Charts-Generated Vertical Bar Chart



Google Charts-Generated Vertical Bar Chart



As you can see in the screen snapshots, there are some slight differences in the implementations of Eastwood Charts and Google Charts. However, they are remarkably similar and it is pretty convenient to be able to have the same code call either implementation to generate these charts. There are many more types of charts available than those shown in this example. Examples of these types of charts can be found in the Google Charts API page and in the Eastwood Chart Servlet samples page.

Monday, August 4, 2008

Using Orson Chart Beans with JConsole Custom Plugin

In this blog entry, I will be looking at using Orson Chart Beans to generate charts in a custom tab added to JConsole. Orson Charts (also sometimes called JFreeBeans in some of the documentation) are backed by JFreeChart and present an easier approach to including JFreeChart-generated charts in Swing applications than using JFreeChart directly.

I could have generated the charts shown in this blog entry's custom JConsole tab using JFreeChart directly (see Example 3/Listing 4 in the article Visualize Your Oracle Database Data with JFreeChart article for an example of using JFreeChart directly in Swing). However, I am using Orson Charts so that I can demonstrate their use and so that more emphasis can stay on implementing the JConsole custom tab.

Java SE 6 provides new features (service provider lookup and SwingWorker) that enable custom tabs in JConsole. The Sun Java SE 6 JDK distribution includes an example custom tab implementation called JTop that demonstrates how to take advantage of the service provider lookup and SwingWorker to create custom tabs for JConsole.

For my example, I'm not dynamically changing the data (for simplicity) and so I don't need to implement as many methods as does the JTop example. Instead, I am building the Orson charts with static, hard-coded data to simplify the examples. However, in a real implementation, one would likely use dynamic data gathered from various sources to build the charts. Data might be retrieved related to databases, networks, file systems, etc., to complement the JVM data already made available in JConsole.

The next class, OrsonBasedJConsolePlugin, shows the simple code needed to create a JConsole Plugin. Most of the interesting functionality is actually in a class this class references.

OrsonBasedJConsolePlugin

package dustin.jconsole.orson;

import com.sun.tools.jconsole.JConsolePlugin;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.swing.JPanel;
import javax.swing.SwingWorker;

/**
* This is a simplistic example of extending JConsole with a custom plugin
* that takes advantage of the Orson (JFreeBeans).
*/
public class OrsonBasedJConsolePlugin
extends JConsolePlugin implements PropertyChangeListener
{
private OrsonPanel orsonPanel = null;
private Map<String, JPanel> customTabs = null;

public OrsonBasedJConsolePlugin()
{
addContextPropertyChangeListener(this);
}

@Override
public Map<String, JPanel> getTabs()
{
if ( customTabs == null )
{
// Using LinkedHashMap because it is a Map implementation that
// maintains its predictable order (usually the order of insertion
// into the LinkedHashMap. Using the LinkedHashMap ensures that the
// new custom tabs appear in the desired order in JConsole.
customTabs = new LinkedHashMap();
orsonPanel = new OrsonPanel();
}

customTabs.put("Orson-Based Chart", new OrsonPanel());
return customTabs;
}

@Override
public SwingWorker<?, ?> newSwingWorker()
{
return orsonPanel.newSwingWorker();
}

@Override
public void propertyChange(PropertyChangeEvent evt)
{
// Implement behavior upon property change.
}
}


The next class is the OrsonPanel class that actually does most of the work related to Orson Charts generation. This panel was created in NetBeans 6.1 GUI builder and some of the "fold" comments indicate this. See the Java SE 6-provided JTop demonstration for a more complete demonstration of implementation of a SwingWorker. The image before the code listing shows how it appears in NetBeans. Note that the generic images for Orson bar chart and Orson pie chart help see what will go where on the final HMI.



OrsonPanel

package dustin.jconsole.orson;

//import java.util.concurrent.ExecutionException;
import javax.swing.SwingWorker;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;

/**
* Simple JPanel using a chart generated JFreeChart-based Orson library.
*/
public class OrsonPanel extends javax.swing.JPanel
{
/** Creates new form OrsonPanel */
public OrsonPanel()
{
initComponents();
populatePieChart();
populateUsersMachinesBarChart();
}

/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {

orsonUsersMachineBarChartPanel = new javax.swing.JLabel();
orsonBarChart = new org.jfree.beans.JBarChart();
orsonPieChart = new org.jfree.beans.JPieChart();

orsonUsersMachineBarChartPanel.setText("Displaying Charts in JConsole with JFreeChart-based Orson is Easy!");

javax.swing.GroupLayout orsonBarChartLayout = new javax.swing.GroupLayout(orsonBarChart);
orsonBarChart.setLayout(orsonBarChartLayout);
orsonBarChartLayout.setHorizontalGroup(
orsonBarChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 360, Short.MAX_VALUE)
);
orsonBarChartLayout.setVerticalGroup(
orsonBarChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 230, Short.MAX_VALUE)
);

javax.swing.GroupLayout orsonPieChartLayout = new javax.swing.GroupLayout(orsonPieChart);
orsonPieChart.setLayout(orsonPieChartLayout);
orsonPieChartLayout.setHorizontalGroup(
orsonPieChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 360, Short.MAX_VALUE)
);
orsonPieChartLayout.setVerticalGroup(
orsonPieChartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGap(0, 230, Short.MAX_VALUE)
);

javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(orsonBarChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(orsonPieChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(orsonUsersMachineBarChartPanel)
.addGap(197, 197, 197))))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(orsonUsersMachineBarChartPanel)
.addGap(18, 18, 18)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(orsonBarChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(orsonPieChart, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addContainerGap(27, Short.MAX_VALUE))
);
}// </editor-fold>


/**
* Populate Orson/JFreeChart-based pie chart.
*/
private void populatePieChart()
{
final DefaultPieDataset data = new DefaultPieDataset();
data.setValue("Opened", 23);
data.setValue("Closed", 10);
this.orsonPieChart.setDataset(data);
this.orsonPieChart.setTitle("Database Connections");
this.orsonPieChart.setSubtitle("Opened and Closed Database Connections");
}

/**
* Populate Orson/JFreeChart-based bar chart that displays numbers of types
* of users on each machine.
*/
private void populateUsersMachinesBarChart()
{
final DefaultCategoryDataset data = new DefaultCategoryDataset();
final String PRIVILEGED_USERS = "Privileged Users";
final String REGULAR_USERS = "Regular Users";
data.setValue(7, PRIVILEGED_USERS, "host1" );
data.setValue(5, PRIVILEGED_USERS, "host2" );
data.setValue(50, REGULAR_USERS, "host1" );
data.setValue(38, REGULAR_USERS, "host2");
this.orsonBarChart.setDataset(data);
this.orsonBarChart.setTitle("Types of Users Using Machines");
this.orsonBarChart.setSubtitle("Privileged and Regular Users");
this.orsonBarChart.setCategoryAxisLabel("Machine Hosts");
this.orsonBarChart.setValueAxisLabel("Number of Users");
}


/** SwingWorker used exclusively for JConsolePlugin. */
class OrsonWorker extends SwingWorker
{
OrsonWorker() {}

public CategoryDataset doInBackground()
{
final DefaultCategoryDataset data = new DefaultCategoryDataset();
return data;
}

@Override
protected void done()
{
}
}

// Return a new SwingWorker for UI update
public SwingWorker<?,?> newSwingWorker()
{
return new OrsonWorker();
}


// Variables declaration - do not modify
private org.jfree.beans.JBarChart orsonBarChart;
private org.jfree.beans.JPieChart orsonPieChart;
private javax.swing.JLabel orsonUsersMachineBarChartPanel;
// End of variables declaration
}


To use the service provider mechanism to allow JConsole to detect the custom tab implemented above, a special file must be included in the JAR with the two above classes. This special file must have the name >com.sun.tools.jconsole.JConsolePlugin and must contain the fully qualified name of the plugin class. In other words, for our example with only a single plugin, it should contain the following line:

com.sun.tools.jconsole.JConsolePlugin

dustin.jconsole.orson.OrsonBasedJConsolePlugin


A single JAR with the compiled versions of the above classes and the just-mentioned file should be built. This will be provided to JConsole when it is run. The JAR contents look like this in my example:

Contents of JConsoleWithOrson.jar

0 Sun Aug 03 21:39:48 MDT 2008 META-INF/
162 Sun Aug 03 21:39:46 MDT 2008 META-INF/MANIFEST.MF
0 Sun Aug 03 21:17:56 MDT 2008 META-INF/services/
0 Sun Aug 03 21:17:14 MDT 2008 dustin/
0 Sun Aug 03 21:17:14 MDT 2008 dustin/jconsole/
0 Sun Aug 03 21:17:14 MDT 2008 dustin/jconsole/orson/
46 Sun Aug 03 21:17:56 MDT 2008 META-INF/services/com.sun.tools.jconsole.JConsolePlugin
1458 Sun Aug 03 21:17:14 MDT 2008 dustin/jconsole/orson/OrsonBasedJConsolePlugin.class
1095 Sun Aug 03 21:39:48 MDT 2008 dustin/jconsole/orson/OrsonPanel$OrsonWorker.class
4863 Sun Aug 03 21:39:48 MDT 2008 dustin/jconsole/orson/OrsonPanel.class


While JConsole can normally be run by simply typing "jconsole" at the command-line, we now need to provide this JAR file, JConsoleWithOrson.jar, to the "jconsole" command. In addition, we also need to include Orson and JFreeChart libraries used to JConsole as well to avoid exceptions related to not finding these classes. The run command to run JConsole with this custom plugin JAR and associated Orson and JFreeChart libraries is:


jconsole -pluginpath JConsoleWithOrson.jar -J-Djava.class.path="C:\Program Files\Java\jdk1.6.0_07\lib\jconsole.jar";"C:\Program Files\Java\jdk1.6.0_07\lib\tools.jar";C:\orson-0.5.0\orson-0.5.0.jar;C:\orson-0.5.0\lib\jfreechart-1.0.6.jar;C:\orson-0.5.0\lib\jcommon-1.0.10.jar


You can see from the command for running JConsole above that I am using the latest version of Orson Charts (0.5). I am also using the version of JFreeChart and JCommon that come with the Orson distribution. There are newer versions of JFreeChart (1.0.10) available, but I wanted to use the version that came with the Orson download. Note also that if you did use JDBC database connections in a real situation, you'd need JDBC drivers and other database accessing classes as well.

When the JAR is compiled as described above and run as shown above, a new tab appears on JConsole as shown here:



Even with all the detail I tried to include in this blog entry, I hope that it is evident how easy it is to use Orson to add charts to your custom JConsole plug-in tabs.

Monday, January 14, 2008

JFreeChart 1.0.9 Released

Dave Gilbert recently announced the release of JFreeChart 1.0.9. The release notes for this release are conveniently available in the General JFreeChart Forum.

I am pleased to report that the sample code associated with the Oracle Technology Network (OTN) article Visualize Your Oracle Database Data with JFreeChart works properly using this new release.

Thursday, November 29, 2007

JFreeChart 1.0.8 Released

Dave Gilbert recently announced the release of JFreeChart 1.0.8. In my tests, all of the sample code from Visualize Your Oracle Database Data with JFreeChart worked correctly against this new release. This is not very surprising because the 1.0.8 changes sound like mostly fixes to bugs introduced with JFreeChart 1.0.7 changes.

UPDATE: A 1.0.8a (7 December 2007) version of JFreeChart is now available online at http://sourceforge.net/project/showfiles.php?group_id=15494. It appears that this version may have been released to fix the security vulnerability associated with HTML image maps originally discovered and identified by Rapid7.

Monday, November 26, 2007

JAXX and JFreeChart

JAXX is an XML-based user interface framework for developing Swing applications. As such, JAXX looks and feels like OpenLaszlo's LZX language and Flex's MXML language. Like LZX and MXML, JAXX code is written primarily in XML and supports scripting in special tags within the XML. In the case of JAXX, the scripting language is Java (compared to a JavaScript subset for OpenLaszlo and to ActionScript 3 for Flex 2 MXML).

JAXX shares other characteristics with OpenLaszlo LZX and Flex MXML. All three frameworks support an id attribute to uniquely identify each tag in the XML. The id attribute provides a handle for code in the script to access what is in the XML elements. JAXX also supports data binding, a very nifty feature of both Flex MXML and OpenLaszlo LZX. In fact, its syntax is exactly like Flex's MXML binding syntax (use of curly braces { and } around binding) and very similar to OpenLaszlo's LZX binding syntax (curly braces with $ in front similar to Ant).

In our article Visualize Your Oracle Database Data with JFreeChart, Michael Martin and I demonstrated an extremely simple example of using JFreeChart in conjunction with a Swing application (Code Listing 4 and Figure 7). In this blog entry, I will be adapting that example to use JAXX rather than direct Swing (JAXX compiles into Swing).

JAXX can be downloaded at http://www.jaxxframework.org/wiki/Download and can be installed as described at http://www.jaxxframework.org/wiki/Installing_JAXX. The version of JAXX that I used for this example is JAX 1.0.2. The zip file you download for this is well under 1 MB. Installation consists of unzipping the downloaded file (expanded directories and files still come in under 1 MB), setting JAVA_HOME to point to your JDK directory (this may already be the case on your system for Ant or other Java-based applications to use), and placing the JAXX installation's bin directory in your environment's path.

The following code listing shows the JAXX code for my file jaxxexample.jaxx (notice the extension .jaxx).

<Application title="Jaxx Example Using JFreeChart"
width="750"
height="450">
<JLabel id="mainLabel"
icon="{generateBarChartImage()}" />
<script>
private ImageIcon generateBarChartImage()
{
org.marx.hr.charting.HrChartCreator chartCreator =
new org.marx.hr.charting.HrChartCreator("");
org.jfree.chart.JFreeChart barChart =
chartCreator.createSalaryPerFinanceEmployeeBarChart(
org.jfree.chart.plot.PlotOrientation.VERTICAL );
return new ImageIcon(barChart.createBufferedImage(750,450));
}
</script>
</Application>


The above code is compiled using the following syntax (assuming that JAXX has been downloaded and installed as described previously):


jaxxc -cp "C:\jfreechart\articleSourceCode\visualizeOracleDb\build\classes;C:\jfreechart\jfreechart-1.0.7\lib\jfreechart-1.0.7.jar" -keep jaxxexample.jaxx


There are several things worth special mention regarding the jaxxc compilation shown above.

First, jaxxc works much like javac in terms of available options. The -keep option keeps the intermediate .java files and another option (-java, not shown here) only generates the .java files without building the corresponding .class files.

Second, I needed to place quotation marks around my classpath after the -cp if I had more than one path in the classpath designation. A single path in the classpath worked appropriately (compiled), but having more than one without the quotes led the JAXX compiler to showing its usage instead of compiling the .jaxx file. For this example, I did need two paths: one to the Java class with the chart generation code and one to the JFreeChart library.

After the .class files have been generated (and in this case, the .java file was retained as well), the code needs to be run to show JAXX in action. The following command was used to run this code:


java -cp .;C:\jaxx-1.0.2\lib\jaxx-runtime.jar;C:\jfreechart\articleSourceCode\visualizeOracleDb\build\classes;C:\jfreechart\jfreechart-1.0.7\lib\jfreechart-1.0.7.jar;C:\jfreechart\jfreechart-1.0.7\lib\jcommon-1.0.12.jar;C:\batik-1.7\batik.jar;C:\iText\itext-2.0.2.jar;C:\jdevstudio1111\jdbc\lib\*;C:\jdevstudio1111\lib\* jaxxexample


The highlighted portion of the classpath shows that the JAXX runtime JAR is necessary for running this code compiled with jaxxc. Several other libraries must be added to this classpath as well because the included chart generation class access iText (for PDF), Batik (for SVG), and JDeveloper (for Oracle datasource) libraries. Notice that no quotations are needed for multiple paths in this classpath because this is a normal Java launcher executable. Quotes are not needed for Java's javac and java commands to use multiple paths in their classpaths.

When the above command is run with the Java application launcher, output similar to that shown in the next snapshot should be displayed (click on image to see larger version):



Not surprisingly, this image looks very similar to that in Figure 7 of the article that was generated with straight Swing. This is not surprising because JAXX compiles first to Swing. Ideally, I'd have most of the code in the <script> tag in the JAXX example in a separate Java class so that very little scripting would be in the file and it would be almost all XML.

There is much more to JAXX than what is shown here. This blog entry is merely an introduction to an approach to non-web Java HMIs that is very similar to that used in OpenLaszlo and Flex.

I have just started to investigate JAXX, but have already made a few observations in addition to those listed above that are worth consideration.

  • Perhaps the best method for further investigating JAXX is to read the newbie-friendly article Introducing JAXX: A New Way To Swing. This article is written by JAXX's author, Ethan Nicholas.

  • Ethan Nicholas also presented on JAXX at JavaOne 2006: User Interfaces in XML: The JAXX Framework.

  • It appears that JAXX has not been significantly supported in over a year. The last "News" release on the main page (as of today, November 26, 2007) is dated October 9, 2006, and announces the release of JAX 1.0.3-beta (I'm using JAXX 1.0.2 final for this article and that was announced in August 2006). Similarly, the most recent entry in the JAXX Announcements Forum is dated June 12, 2006, and announces JAXX 1.0.1. These observations, coupled with related observations noted below, create some questions about the current and future state of JAXX.

  • The JAXX Main Page is a Wiki and it seems that someone thought it was a good idea to add links to various lewd messages at the top of many of the JAXX Wiki pages.

  • The JAXX General Chat Forum has over 4000 messages, but this seems to be mostly due to it being overrun by spam.

  • The JAXX How Do I ... Forum does have some non-spam, fairly recent postings, but they are not numerous.

  • In Swing: Its Past, Present, and Future, Hans Muller has nice things to say about JAXX.

  • Having significantly more experience with XML-oriented presentation languages like JavaServer Pages (JSPs), LZX, and MXML than with Java Swing, there is much conceptually about JAXX that appeals to me. I really like the binding support in OpenLaszlo and Flex and like the idea of a Java framework providing this for simple but powerful event handling. However, a significant hurdle for JAXX is lack of the wide community support that MXML and LZX enjoy.

  • A common complaint in OpenLaszlo and Flex forums is the lack of an IDE or lack of features in the available IDEs for developing in these environments or the cost of any available IDEs. JAXX suffers from lack of an IDE and this is especially noticeable because of the powerful Java IDEs that are available and provide Swing GUI builders. This and other limitations are also noted in "JAXX ... Oh My."

Saturday, November 17, 2007

Simple Pie Chart Labels in JFreeChart 1.0.7

In the OTN article Visualize Your Oracle Database Data with JFreeChart, we showed multiple examples using JFreeChart to generate pie charts. However, in all the pie charts in this article and its accompanying source code, the labels for the pie chart sections were displayed outside of the actual pie and had lines connecting the labels to the appropriate pie section.

Recently released JFreeChart 1.0.7 makes it easy to place these pie section labels directly on the applicable pie slices. The following code snippet is taken from Listing 18 in the OTN article and the one additional line necessary to use "simple" labels rather than "extended" labels for a JFreeChart pie chart is highlighted:

/**
* Create 3D pie chart displaying the number of employees at each
* commission level. This method demonstrates use of DatasetReader to
* translate data from XML format to JFreeChart data format.
*
* @return 3D Pie Chart showing number of employees at each commission
* level.
*/
public JFreeChart createCommissionsPercentagePerLevelPieChart()
{
PieDataset pieData = null;

final String xmlData = databaseAccess.getCommissionPercentageBreakdownAsXml();

final DatasetReader reader = new DatasetReader();
try
{
pieData = reader.readPieDatasetFromXML(
new ByteArrayInputStream(xmlData.getBytes()) );
}
catch (IOException ioEx)
{
ioEx.printStackTrace();
}

JFreeChart chart =
ChartFactory.createPieChart3D( "Commissioned Employees at Each Commission Level",
pieData,
false, true, false );

// Chart specifically provides getCategoryPlot() and getXYPlot() methods,
// but not a getPiePlot() or getPiePlot3D() method.
final PiePlot3D plot = (PiePlot3D) chart.getPlot();
plot.setDepthFactor(0.35); // pie depth 35% of plot height
plot.setCircular(false);
plot.setForegroundAlpha(0.5f); // Declare explicitly as float (50%)

plot.setLabelGenerator(new HrCommissionsPieGenerator());

plot.setSimpleLabels(true);

return chart;
}


In the above example, a PiePlot3D was already being used, so only one line of new code was needed to move the pie section labels onto the sections themselves. This line was plot.setSimpleLabels(true).

There are a couple interesting side notes to make here. First, this also works with PiePlot even though PiePlot3D was used in the example above. Second, passing false to the setSimpleLabels method is the equivalent of not calling this method at all. In other words, not using simple labels (using "extended" labels) is the default. This makes sense because this was the case for JFreeChart 1.0.5 and JFreeChart 1.0.6 (the versions we wrote our article against) and the behavior remains the default.

I also like the default being "extended" pie section labels rather than "simple" pie section labels because the labels start appearing on top of each other fairly quickly when there are numerous or very small pie sections. The following image (click on it to see larger version) shows what the chart shown in the article's Figure 15 looks like with "simple" labels rather than the extended labels used (by default) in the article.



The simple labels look pretty good here because of the relatively small number of pie sections and the relatively large size of each section that provides sufficient room to place the labels within the section. However, even in this example, a few of the labels do overlap each other.

Transparent Plot Backgrounds in JFreeChart

Code Listing 13 (and Figures 13 and 14) of the OTN article Visualize Your Oracle Database Data with JFreeChart demonstrated how to make a chart's background transparent. In that example, we did not make the plot background transparent because the different color for the plot area often provides a nice offset to the page background. Sometimes, however, you want the entire background, including the plot area, to be transparent. Fortunately, this is very easy to accomplish.

In the following code listing, the method writePngTransparentBasedOnChart() has been enhanced to make the background of plot area transparent if the last parameter passed to the method is true.

/**
 * Write .png file with transparent background based on provided JFreeChart.
 * 
 * @param aChart JFreeChart.
 * @param aFileName Name of file to which JFreeChart.
 * @param aWidth Width of image.
 * @param aHeight Height of image.
 * @param aPlotTransparent Should plot area be transparent?
 */
public void writePngTransparentBasedOnChart( final JFreeChart aChart,
                                             final String aFileName,
                                             final int aWidth,
                                             final int aHeight,
                                             final boolean aPlotTransparent )
{
   final String fileExtension = ".png";
   final String writtenFile =  destinationDirectory
                      + aFileName
                      + fileExtension;
   try
   {
      aChart.setBackgroundPaint( new Color(255,255,255,0) );
      if ( aPlotTransparent )
      {
         final Plot plot = aChart.getPlot();
         plot.setBackgroundPaint( new Color(255,255,255,0) );
         plot.setBackgroundImageAlpha(0.0f);
      }

      final CategoryItemRenderer renderer = aChart.getCategoryPlot().getRenderer();
      renderer.setSeriesPaint(0, Color.blue.brighter());
      renderer.setSeriesVisible(0, true); // default
      renderer.setSeriesVisibleInLegend(0, true);  // default

      ChartUtilities.writeChartAsPNG( new FileOutputStream(writtenFile),
                                      aChart,
                                      aWidth, aHeight,
                                      null,
                                      true,    // encodeAlpha
                                      0 );
      System.out.println("Wrote PNG (transparent) file " + writtenFile);
   }
   catch (IOException ioEx)
   {
      System.err.println(  "Error writing PNG file " + writtenFile + ": "
                  + ioEx.getMessage() );
   }
}

Only a few extra lines were necessary to make the background transparent and these are highlighted. The produced chart with even the plot area transparent is shown in the figure below. This can be contrasted with Figures 13 and 14 in the original article to more easily contrast the difference in the plot area portion of the chart. Click on the image below to see it in larger form.

As I stated previously, I often like to have the plot area have a different background color than the page around it for the offset effect, but there are situations where it is nice to have the chart blend more fully into the background page. In such cases, as the listing above demonstrates, it is simple to make the plot areas transparent.

As a reminder, this works because the PNG (Portable Network Graphics) format supports alpha channel transparency.

JFreeChart 1.0.7 Release and OTN JFreeChart Article

Dave Gilbert recently announced the release of JFreeChart 1.0.7. The code sample included with the Oracle Technology Network article "Visualize Your Oracle Database Data with JFreeChart" seems to build and run correctly without change with this new release of JFreeChart.

Friday, October 19, 2007

Adding Gradients to JFreeChart Charts

In the article Visualize Your Oracle Database Data with JFreeChart published on Oracle Technology Network, Michael Martin and I covered several of JFreeChart’s useful features, but we were unable to cover everything this broad library provides in a reasonably sized article. This blog entry focuses on an aspect of JFreeChart that we did not cover in that article: gradients.

Our third example in the article covered how to use JFreeChart in a Swing-based application and rendered a bar chart for the example. In this blog, I show how easy it is to modify that example to use gradients.

The basic Swing class used to display the JFreeChart-generated bar chart is shown here:


package org.marx.hr.charting;

import java.awt.image.BufferedImage;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;

import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;

/**
* Class that produces simple Swing application to render a JFreeChart-generated bar chart.
*/
public class HrChartingSwingRenderer
{
/**
* Create the GUI and show it. For thread safety,
* this method should be invoked from the
* event-dispatching thread.
*/
private static void createSimpleDemonstrativeLabel(
final String aTitle)
{
HrChartCreator chartCreator = new HrChartCreator();
JFrame frame = new JFrame(aTitle);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

JFreeChart barChart =
chartCreator.createSalaryPerFinanceEmployeeBarChart(
PlotOrientation.VERTICAL );

BufferedImage image =
barChart.createBufferedImage(750,450);
JLabel label = new JLabel();
label.setIcon(new ImageIcon(image));
frame.getContentPane().add(label);

frame.pack();
frame.setVisible(true);
}

public static void main(String[] args)
{
javax.swing.SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
createSimpleDemonstrativeLabel(
"Swing Example Using JFreeChart: "
+ "Salary Per Finance Employee");
}
});
}
}


As shown in this Swing-based class, the HrChartCreator class is used for generating the bar chart. Specifically, the createSalaryPerFinanceEmployeeBarChart method is called on that class. The code listing for that method as used in the article example is shown next:


/**
* Create Bar Chart showing salary of each employee.
*
* @param aOrientation Horizontal or Vertical
* orientation of bar chart.
* @return Bar Chart.
*/
public JFreeChart createSalaryPerFinanceEmployeeBarChart(
final PlotOrientation aOrientation)
{
JFreeChart barChart = null;
public static final String QUERY_SALARY_PER_FINANCE_EMPLOYEE =
"SELECT first_name || ' ' || last_name AS Name, salary " +
"FROM employees " +
"WHERE department_id = 100";

try
{
final CategoryDataset barDataset =
new JDBCCategoryDataset(
databaseAccess.getOracleDbConnection(),
QUERY_SALARY_PER_FINANCE_EMPLOYEE );

barChart =
ChartFactory.createBarChart(
"Finance Department Employees Salaries", // chart title
"Finance Employees",
"Salaries",
barDataset,
aOrientation,
true, // legend displayed
true, // tooltips displayed
false ); // no URLs

}
catch (SQLException sqlEx)
{
System.err.println(
"Error trying to acquire JDBCCategoryDataset.");
System.err.println(
"Error Code: " + sqlEx.getErrorCode());
System.err.println( "SQLSTATE: "
+ sqlEx.getSQLState());
sqlEx.printStackTrace();
}

return barChart;
}


In the example in the article, Figure 7 shows a snapshot of the Swing application with the JFreeChart-generated bar chart. The bars in the bar chart use the default first color (red) for the bars and the bars are solid in color. A gradient could be used here to make the chart a little more interesting and possible even a little more aesthetically pleasing.

Only a few additional lines are needed to add gradients to the bars in this chart. Had we been using a CategoryPlot for other purposes anyway, even fewer individual lines would be needed because we would have already had access to the CategoryPlot and possibly to the BarRenderer. The code listing below shows the method with gradients added. The newly added lines for gradient support are highlighted.


/**
* Create Bar Chart showing salary of each employee.
*
* @param aOrientation Horizontal or Vertical
* orientation of bar chart.
* @return Bar Chart.
*/
public JFreeChart createSalaryPerFinanceEmployeeBarChart(
final PlotOrientation aOrientation)
{
JFreeChart barChart = null;
public static final String QUERY_SALARY_PER_FINANCE_EMPLOYEE =
"SELECT first_name || ' ' || last_name AS Name, salary " +
"FROM employees " +
"WHERE department_id = 100";

try
{
final CategoryDataset barDataset =
new JDBCCategoryDataset(
databaseAccess.getOracleDbConnection(),
QUERY_SALARY_PER_FINANCE_EMPLOYEE );

barChart =
ChartFactory.createBarChart(
"Finance Department Employees Salaries, // chart title
"Finance Employees",
“Salaries”,
barDataset,
aOrientation,
true, // legend displayed
true, // tooltips displayed
false ); // no URLs

final CategoryPlot plot = barChart.getCategoryPlot();
final BarRenderer renderer = (BarRenderer) plot.getRenderer();
final GradientPaint gradient =
new GradientPaint( 0.0f, 0.0f, Color.RED.brighter(),
0.0f, 0.0f, Color.WHITE );
renderer.setSeriesPaint(0, gradient);

}
catch (SQLException sqlEx)
{
System.err.println(
"Error trying to acquire JDBCCategoryDataset.");
System.err.println( "Error Code: "
+ sqlEx.getErrorCode());
System.err.println( "SQLSTATE: "
+ sqlEx.getSQLState());
sqlEx.printStackTrace();
}

return barChart;
}


The new version of the bar chart with gradients is shown next (click on image to see larger version):



With just a few extra lines of code we were able to easily enhance our bar chart to use a gradient rather than solid color for its bars.