Thursday, January 24, 2013

JavaFX 2 XYCharts and Java 7 Features

One of my favorite features of JavaFX 2 is the standard charts it provides in its javafx.scene.chart package. This package provides several different types of charts out-of-the-box. All but one of these (the PieChart) are "2 axis charts" (specific implementations of the XYChart). In this post, I look at the commonality between these specializations of XYChart. Along the way, I look at several Java 7 features that come in handy.

A UML class diagram for key chart types in the javafx.scene.chart package is shown next. Note that AreaChart, StackedAreaChart, BarChart, StackedBarChart, BubbleChart, LineChart, and ScatterChart all extend XYChart.

As the UML diagram above (generated using JDeveloper) indicates, the PieChart extends Chart directly while all the other chart types extend XYChart. Because all the chart types other than PieChart extend XYChart, they share some common features. For example, they are all 2-axis charts with a horizontal ("x") axis and a vertical ("y") axis. They generally allow data to be specified in the same format (data structure) for all the XY charts. The remainder of this post demonstrates being able to use the same data for most of the XYCharts.

The primary use of a chart is to show data, so the next code listing indicates retrieving of data from the 'hr' sample schema in an Oracle database. Note that JDBC_URL, USERNAME, PASSWORD, and AVG_SALARIES_PER_DEPARTMENT_QUERY are constant Strings used in the JDBC connection and for the query.

getAverageDepartmentsSalaries()
/**
 * Provide average salary per department name.
 * 
 * @return Map of department names to average salary per department.
 */
public Map<String, Double> getAverageDepartmentsSalaries()
{
   final Map<String, Double> averageSalaryPerDepartment = new HashMap<>();
   try (final Connection connection = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
        final Statement statement = connection.createStatement();
        final ResultSet rs = statement.executeQuery(AVG_SALARIES_PER_DEPARTMENT_QUERY))
   {
      while (rs.next())
      {
         final String departmentName = rs.getString(COLUMN_DEPARTMENT_NAME);
         final Double salaryAverage = rs.getDouble(ALIAS_AVERAGE_SALARY);
         averageSalaryPerDepartment.put(departmentName, salaryAverage);
      }
   }
   catch (SQLException sqlEx)
   {
      LOGGER.log(
         Level.SEVERE,
         "Unable to get average salaries per department - {0}", sqlEx.toString());
   }
   return averageSalaryPerDepartment;
}

The Java code snippet above uses JDBC to retrieve data for populating a Map of department name Strings to the average salary of the employees in each department. There are a couple of handy Java 7 features used in this code. A small feature is the inferred generic parameterized typing of the diamond operator used with the declaration of the local variable averageSalaryPerDepartment (line 8). This is a small granule of syntax sugar, but it does make the code more concise.

A more significant Java 7 feature is use of try-with-resources statement for the handling of the Connection, Statement, and ResultSet resources (lines 9-11). This is a much nicer way to handle the opening and closing of these resources, even in the face of exceptions, than was previously necessary when using JDBC. The Java Tutorials page on The try-with-resources Statement advertises that this statement "ensures that each resource is closed at the end of the statement" and that each resource will "be closed regardless of whether the try statement completes normally or abruptly." The page also notes that when there are multiple resources specified in the same statement as is done in the above code, "the close methods of resources are called in the opposite order of their creation."

The data retrieved from the database can be placed into the appropriate data structure to support use by most of the XYCharts. This is shown in the next method.

ChartMaker.createXyChartDataForAverageDepartmentSalary(Map)
/**
 * Create XYChart Data representing average salary per department name.
 * 
 * @param newAverageSalariesPerDepartment Map of department name (keys) to
 *    average salary for each department (values).
 * @return XYChart Data representing average salary per department.
 */
public static ObservableList<XYChart.Series<String, Double>> createXyChartDataForAverageDepartmentSalary(
   final Map<String, Double> newAverageSalariesPerDepartment)
{
   final Series<String, Double> series = new Series<>();
   series.setName("Departments");
   for (final Map.Entry<String, Double> entry : newAverageSalariesPerDepartment.entrySet())
   {
      series.getData().add(new XYChart.Data<>(entry.getKey(), entry.getValue()));
   }
   final ObservableList<XYChart.Series<String, Double>> chartData =
      FXCollections.observableArrayList();

   chartData.add(series);
   return chartData;
}

The method just shown places the retrieved data in a data structure that can be used by nearly all of the XYChart-based charts. With the retrieved data now packaged in a JavaFX observable collection, the charts can be easily generated. The next code snippet shows methods for generating several XYChart-based charts (Area, Bar, Bubble, Line, and Scatter). Note how similar they all are and how the use the same data provided by the same method. The StackedBar and StackedArea charts can also use similar data, but are not shown here because they are not interesting for the single series of data being used in this example.

Methods for Generating XYCharts Except BubbleChart and Stacked Charts
private XYChart<String, Double> generateAreaChart(
   final Axis<String> xAxis, final Axis<Double> yAxis)
{
   final AreaChart<String, Double> areaChart =
      new AreaChart<>(
         xAxis, yAxis,
         ChartMaker.createXyChartDataForAverageDepartmentSalary(
            this.databaseAccess.getAverageDepartmentsSalaries()));
   return areaChart;
}

private XYChart<String, Double> generateBarChart(
   final Axis<String> xAxis, final Axis<Double> yAxis)
{
   final BarChart<String, Double> barChart =
      new BarChart<>(
         xAxis, yAxis,
         ChartMaker.createXyChartDataForAverageDepartmentSalary(
            this.databaseAccess.getAverageDepartmentsSalaries()));
   return barChart;
}

private XYChart<Number, Number> generateBubbleChart(
   final Axis<String> xAxis, final Axis<Double> yAxis)
{
   final Axis<Number> deptIdXAxis = new NumberAxis();
   deptIdXAxis.setLabel("Department ID");
   final BubbleChart<Number, Number> bubbleChart =
      new BubbleChart(
         deptIdXAxis, yAxis,
         ChartMaker.createXyChartDataForAverageDepartmentSalaryById(
            this.databaseAccess.getAverageDepartmentsSalariesById()));
   return bubbleChart;
}

private XYChart<String, Double> generateLineChart(
        final Axis<String> xAxis, final Axis<Double> yAxis)
{
   final LineChart<String, Double> lineChart =
      new LineChart<>(
         xAxis, yAxis,
         ChartMaker.createXyChartDataForAverageDepartmentSalary(
            this.databaseAccess.getAverageDepartmentsSalaries()));
   return lineChart;
}

private XYChart<String, Double> generateScatterChart(
   final Axis<String> xAxis, final Axis<Double> yAxis)
{
   final ScatterChart<String, Double> scatterChart =
      new ScatterChart<>(
         xAxis, yAxis,
         ChartMaker.createXyChartDataForAverageDepartmentSalary(
            this.databaseAccess.getAverageDepartmentsSalaries()));
   return scatterChart;
}

These methods are so similar that I could have actually used method handles (or more traditional reflection APIs) to reflectively call the appropriate chart constructor rather than use separate methods. However, I am using these for my RMOUG Training Days 2013 presentation in February and so wanted to leave the chart-specific constructors in place to make them clearer to audience members.

One exception to the general handling of XYChart types is the handling of BubbleChart. This chart expects a numeric type for its x-axis and so the String-based (department name) x-axis data provided above will not work. A different method (not shown here) provides a query that returns average salaries by department ID (Long) rather than by department name. The slightly different generateBubbleChart method is shown next.

generateBubbleChart(Axis, Axis)
   private XYChart<Number, Number> generateBubbleChart(
      final Axis<String> xAxis, final Axis<Double> yAxis)
   {
      final Axis<Number> deptIdXAxis = new NumberAxis();
      deptIdXAxis.setLabel("Department ID");
      final BubbleChart<Number, Number> bubbleChart =
         new BubbleChart(
            deptIdXAxis, yAxis,
            ChartMaker.createXyChartDataForAverageDepartmentSalaryById(
               this.databaseAccess.getAverageDepartmentsSalariesById()));
      return bubbleChart;
   }

Code could be written to call each of these different chart generation methods directly, but this provides a good chance to use Java 7's method handles. The next code snippet shows this being done. Not only does this code demonstrate Method Handles, but it also uses Java 7's multi-catch exception handling mechanism (line 77).

/**
 * Generate JavaFX XYChart-based chart.
 * 
 * @param chartChoice Choice of chart to be generated.
 * @return JavaFX XYChart-based chart; may be null.
 * @throws IllegalArgumentException Thrown if the provided parameter is null.
 */
private XYChart<String, Double> generateChart(final ChartTypes chartChoice)
{
   XYChart<String, Double> chart = null;
   final Axis<String> xAxis = new CategoryAxis();
   xAxis.setLabel("Department Name");
   final Axis<? extends Number> yAxis = new NumberAxis();
   yAxis.setLabel("Average Salary");
   if (chartChoice == null)
   {
      throw new IllegalArgumentException(
         "Provided chart type was null; chart type must be specified.");
   }
   else if (!chartChoice.isXyChart())
   {
      LOGGER.log(
         Level.INFO,
         "Chart Choice {0} {1} an XYChart.",
         new Object[]{chartChoice.name(), chartChoice.isXyChart() ? "IS" : "is NOT"});
   }

   final MethodHandle methodHandle = buildAppropriateMethodHandle(chartChoice);
   try
   {
      chart =
        methodHandle != null
      ? (XYChart<String, Double>) methodHandle.invokeExact(this, xAxis, yAxis)
      : null;
      chart.setTitle("Average Department Salaries");
   }
   catch (WrongMethodTypeException wmtEx)
   {
      LOGGER.log(
         Level.SEVERE,
         "Unable to invoke method because it is wrong type - {0}",
         wmtEx.toString());
   }
   catch (Throwable throwable)
   {
      LOGGER.log(
         Level.SEVERE,
         "Underlying method threw a Throwable - {0}",
         throwable.toString());
   }

   return chart;
}

/**
 * Build a MethodHandle for calling the appropriate chart generation method
 * based on the provided ChartTypes choice of chart.
 * 
 * @param chartChoice ChartTypes instance indicating which type of chart
 *    is to be generated so that an appropriately named method can be invoked
 *    for generation of that chart.
 * @return MethodHandle for invoking chart generation.
 */
private MethodHandle buildAppropriateMethodHandle(final ChartTypes chartChoice)
{
   MethodHandle methodHandle = null;
   final MethodType methodDescription =
      MethodType.methodType(XYChart.class, Axis.class, Axis.class);
   final String methodName = "generate" + chartChoice.getChartTypeName() + "Chart";

   try
   {
      methodHandle =
         MethodHandles.lookup().findVirtual(
            this.getClass(), methodName, methodDescription);
   }
   catch (NoSuchMethodException | IllegalAccessException exception)
   {
      LOGGER.log(
         Level.SEVERE,
         "Unable to acquire MethodHandle to method {0} - {1}",
         new Object[]{methodName, exception.toString()});
   }
   return methodHandle;
}

A series of images follows that shows how these XY Charts appear when rendered by JavaFX.

Area Chart
Bar Chart
Bubble Chart
Line Chart
Scatter Chart

As stated above, Method Handles could have been used to reduce the code even further because individual methods for generating each XYChart are not absolutely necessary and could have been reflectively called based on desired chart type. It's also worth emphasizing that if the x-axis data had been numeric, the code would be the same (and could be reflectively called) for all XYChart types including the Bubble Chart.

JavaFX makes it easy to generate attractive charts representing provided data. Java 7 features make this even easier by making code more concise and more expressive and allowing for easy application of reflection when appropriate.

No comments: