Hiển thị các bài đăng có nhãn JFreeChart. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn JFreeChart. Hiển thị tất cả bài đăng

Thứ Hai, 15 tháng 12, 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.

Thứ Hai, 25 tháng 8, 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.

Thứ Hai, 4 tháng 8, 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.