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.

No comments: