What's New In JavaFX 1.2 Technology: JavaFX Charts

   
By Chris Wright and James L. (Jim) Weaver, November 2009  

In the previous article of this series, What's New In JavaFX 1.2 Technology: RSS, Storage, and Charts, the StockReaderFX example application demonstrated the ability to update the UI from RSSTasks, and storing, loading and clearing local data. The StockReaderFX ran RSSTasks based on user-specified stock symbols, parsed the returned title into usable pieces that were used in the UI, and locally stored the users stock symbols that were loaded the next time the program was invoked. This article will integrate the different JavaFX Charts and demonstrate how to use each.

Contents
 
Bar Charts
Pie Charts
Line & Area Charts
Bubble & Scatter Charts
Expanding StockReaderFX
Conclusion
 

Bar Charts

Before diving into each chart, take a look at the following basic application, which allows the user to toggle between each type of graph. It is a simple representation of the JavaFX charts, and demonstrates how the main variables in each chart are used. Here is a screenshot from the application, and the code for the BarChart that is shown:

 
Figure 1: BarChart Example
 
 
/**
 * Categories for the bar chart
 */
def categories:String[] = ["Last Price","New Price"];

/**
 * A list of stock symbols for use in the graphs
 */
def stocks:String[] = ["AAPL","GOOG","MSFT","EBAY"];

/**
 * Simulates the locally stored prices for the stocks
 */
def lastPrice:Number[] = [159.11, 423.59, 97.24, 198.46];

/**
 * Simultes the new prices from a RSSTask
 */
def newPrice:Number[] = [170.38, 434.62, 90.13, 174.77];

var barChart:BarChart = BarChart {
  title: "Stock Prices"
  titleFont: Font { size: 24 }
  visible: bind selectedIndex == 1
  categoryGap: 25
  categoryAxis: CategoryAxis {
    categories: categories
  }
  valueAxis: NumberAxis {
    label: "Price"
    lowerBound: getLowerBound() - 25
    upperBound: getUpperBound() + 25
    tickUnit: 25
  }
  data: for (s in stocks) {
    BarChart.Series {
      name: s
      data: [
        BarChart.Data {
          category: categories[0]
          value: lastPrice[indexof s]
        },
        BarChart.Data {
          category: categories[1]
          value: newPrice[indexof s]
        }
      ]
    }
  }
};
       

Making a JavaFX chart involves very little code. As shown above, we simply give the chart a name, define the chart's axis, and provide a series of data points.

The first major task in creating a chart is defining the x and y axis. Looking at the javafx.scene.chart.part package, 2 axis classes are provided for use here: CategoryAxis and NumberAxis. The CategoryAxis works on a array of string categories, and each category is a unique data point on the axis. In the code sample above, the variable categories is a string array containing ["Last Price","New Price"]. This variable is then assigned to CategoryAxis.categories, creating the x axis visible in Figure 1. The NumberAxis is exactly as its name suggests; an axis based on numeric values. The NumberAxis contains lower and upper bounds, which are the minimum and maximum values of the axis, and a number of variables for controlling major and minor tick units. A major tick is the large, labeled tick, and minor ticks are placed inbetween major ticks. The tickUnit variable allows control over the value between each major tick. For instance, the example above uses a tickUnit of 25, meaning that if the first tick were 0, the next tick would be 25. Note that for upperBound and lowerBound, the application calls the functions getUpperBound and getLowerBound, respectively. These functions simply evaluate sequences of numbers to find the lowest and highest values. See below:

/**
 * Finds the lowest value between both lastPrice and newPrice elements
 */
function getLowerBound():Number {
  var minValues = [];
  insert Sequences.min(lastPrice) into minValues;
  insert Sequences.min(newPrice) into minValues;
  var lowestValue = Sequences.min(minValues as Number[]);
  println("Lowest Value = {lowestValue}");
  return lowestValue as Number
}

/**
 * Finds the highest value between both lastPrice and newPrice elements
 */
function getUpperBound():Number {
  var maxValues = [];
  insert Sequences.max(lastPrice) into maxValues;
  insert Sequences.max(newPrice) into maxValues;
  var highestValue = Sequences.max(maxValues as Number[]);
  println("Highest Value = {highestValue}");
  return highestValue as Number
}
       

Now that the chart's axis are defined, the next task is to create the series of data for the chart. Each chart, except the PieChart, contains a Series class which is a named series of data points for one particular item. For example, "AAPL" is one Series in the above example, which contains two data points (thus, two bars). In the Series, each bar is the same color, which can be changed using the fill variable. Note: The Series class referred to above is not to be confused with javafx.scene.chart.data.Series. Each chart (except PieChart) has a subclass of chart.data.Series. In the example above, it is BarChart.Series.

Table 1: BarChart Variables
Name
Type
Description
categoryAxis
CategoryAxis
The X axis. Its default location is along the bottom of the chart.
categoryGap
Number
The amount of space between each category of bars.
data
Series[]
The chart data series.
title
String
The title of the chart
valueAxis
ValueAxis
The Y axis. Its default location is long the left side of the chart.
 
Table 2: BarChart.Series Variables
Name
Type
Description
data
Data[]
The bar chart data items in this series.
name
String
The displayable name for this series.
strong
Paint
The stroke color of the bars in this series
fill
Paint
The fill color to use for this series
 
Table 3: BarChart.Data Variables
Name
Type
Description
category
String
The category that contains this bar data item, which are shown, by default, along the X axis.
value
Number
The data value to be plotted on the Y axis.
 

Pie Charts

As noted with the BarChart, creating a JavaFX chart requires very little code. Each chart uses many of the same variables, however there are very distinct differences between them (aside from their graphical representation).

 
Figure 2: Pie Charts
 
 

You will notice in Figure 2: Pie Charts that each slice of the pie is clearly labeled, not only with its percentage, but with its Title and Value as well. The code sample below looks almost identical to the BarChart, with the differences mainly lying in the PieChart.Data class, and the absence of any axis.

var pieChart:PieChart = PieChart {
  title: "Stock Prices"
  titleFont: Font { size: 24 }
  visible: bind selectedIndex == 4
  data: for (s in stocks) {
    PieChart.Data {
      value: newPrice[indexof s]
      label: s
    }
  }
};
       

Once again, the chart is given a title and provided a sequence of data. In the case of the PieChart, PieChart.Data is used, which takes a value and a label. The value and label will be displayed outside the chart, with a line connecting to its slice of the pie. The total of all of the PieChart.Data.values makes up the whole circle, and each value is a certain percentage of the circle. Below are the new variables introduced with PieChart.Data.

Table 4: PieChart.Data Variables
Name
Type
Description
resource
Resource
The resource to be managed
source
String
The path (or absolute path) to the resource
 

Line & Area Charts

The Line and Area Charts go hand-in-hand, as the API for each is, once again, practically identical to the other. Take a look at both samples below. Each chart graphs the same lines, with the AreaChart shading the region below each line.

 
Figure 3: Line Charts
 
 
var areaChart:AreaChart = AreaChart {
  title: "Area Chart"
  visible: bind selectedIndex == 0
  xAxis: NumberAxis {
    lowerBound: 1
    upperBound: 2
    tickUnit: 0
  }
  yAxis: NumberAxis {
    lowerBound: getLowerBound() - 25
    upperBound: getUpperBound() + 25
    tickUnit: 25
  }
  data: for (s in stocks) {
    AreaChart.Series {
      name: s
      data: [
        AreaChart.Data {
          xValue: 1
          yValue: lastPrice[indexof s]
        },
        AreaChart.Data {
          xValue: 2
          yValue: newPrice[indexof s]
        }
      ]
    }
  }
};
       
 
Figure 4: Area Charts
 
 
var lineChart:LineChart = LineChart {
  title: "Line Chart"
  visible: bind selectedIndex == 3
  dataEffect: null
  xAxis: NumberAxis {
    lowerBound: 1
    upperBound: 2
    tickUnit: 0
  }
  yAxis: NumberAxis {
    lowerBound: getLowerBound() - 25
    upperBound: getUpperBound() + 25
    tickUnit: 25
  }
  data: for (s in stocks) {
    LineChart.Series {
      name: s
      data: [
        LineChart.Data {
          xValue: 1
          yValue: lastPrice[indexof s]
        },
        LineChart.Data {
          xValue: 2
          yValue: newPrice[indexof s]
        }
      ]
    }
  }
};
       

The Line and Area charts are, for the most part, exactly the same. The main difference, as evident in Figures 3 & 4, is that the Area Chart shades the space in between the lines. Notice above that both axis are NumberAxis. Line and Area charts are required to have NumberAxis, unlike the Bar chart which uses a CategoryAxis as the Y axis. Because all the data in Line and Area charts is numerical, the LineChart.Data class differs from Pie and Bar charts (as seen above) as it takes number values for the X and Y axis.

Once again, the structure of the code looks very similar to Bar and Pie charts, making the creation of different charts very easy. In Tables 5, 6 & 7 below, the variables used in Line and Area charts are defined more clearly.

Table 5: AreaChart Variables
Name
Type
Description
data
Series[]
The chart data series
xAxis
ValueAxis
The X axis, which is along the bottom of the plot by default.
yAxis
ValueAxis
The Y axis, which is along the left side of the plot by default.
title
String
The title of the chart.
 
Table 6: LineChart Variables
Name
Type
Description
data
Series[]
The chart data series
dataEffect
Effect
Effect applied to all plotted lines and symbols
xAxis
ValueAxis
The X axis, which is along the bottom of the plot by default.
yAxis
ValueAxis
The Y axis, which is along the bottom of the plot by default.
 
Table 7: AreaChart.Data & LineChart.Data Variables
Name
Type
Description
xValue
Number
The data value to be plotted on the X axis
yValue
Number
The data value to be plotted on the Y axis
 

Bubble and Scatter Charts

The BubbleChart is very popular because it can provide a third dimension to data reporting. Not only are the x and y axis relative to specific sets of data, the size of each bubble is its own visual representation of some data element. Thus, JavaFX makes another easy-to-create chart, allowing manipulation of the radius of each bubble. The image and code sample below provide an example of a Bubble chart.

 
Figure 5: Bubble Charts
 
 
var bubbleChart:BubbleChart = BubbleChart {
  title: "Bubble Chart"
  visible: bind selectedIndex == 2
  scaleBubbleRadiusUsingAxis: false
  xAxis: NumberAxis {
    upperBound: 1.0
    tickUnit: 0
  }
  yAxis: NumberAxis {
    lowerBound: getLowerBound() - 25
    upperBound: getUpperBound() + 25
    tickUnit: 25
  }
  data: for (s in stocks) {
    BubbleChart.Series {
      name: s
      data: [
        BubbleChart.Data {
          xValue: Math.random()
          yValue: lastPrice[indexof s]
          radius: lastPrice[indexof s] / 10
        },
        BubbleChart.Data {
          xValue: Math.random()
          yValue: newPrice[indexof s]
          radius: newPrice[indexof s] / 10
        }
      ]
    }
  }
};
       

The APIs for the BubbleChart above and ScatterChart below are virtually the same as the other graphs seen so far in this article. The BubbleChart.Data adds the radius variable, which alters the size of each bubble. The Scatter chart is great for visualizing large amount of data points, and to easily demonstrate this, Dean Iverson's code from the Pro JavaFX book was implemented into the Charts Demo application. It uses random math to place 100 bubbles on the chart. See the code below:

 
Figure 6: ScatterChart
 
 
/**
 * Code for the ScatterChart provided by Dean Iverson through the Pro JavaFX Book
 */
var scatterChart:ScatterChart = ScatterChart {
  title: "Scatter Chart"
  visible: bind selectedIndex == 5
  legendVisible: false
  xAxis: NumberAxis {
    label: "X Axis"
    upperBound: 1.0
    tickUnit: 0.25
    formatTickLabel: function(value) {
      "{%.2f value}"
    }
  }
  yAxis: NumberAxis {
    label: "Y Axis"
    upperBound: 1.0
    tickUnit: 0.25
    formatTickLabel: function(value) {
      "{%.2f value}"
    }
  }
  data: ScatterChart.Series {
    data: for (i in [1..100]) {
      ScatterChart.Data {
        xValue: Math.random()
        yValue: Math.random()
      }
    }
  }
};
       

Table 8, 9 & 10 explain in detail the variables used in creating Bubble and Scatter charts.

Table 8: BubbleChart Variables
Name
Type
Description
data
Series[]
The chart data series
dataEffect
Effect
Effect applied to all plotted lines and symbols
xAxis
ValueAxis
The X axis, which is along the bottom of the plot by default.
yAxis
ValueAxis
The Y axis, which is along the bottom of the plot by default.
scaleBubbleRadiusUsingAxis
Boolean
If true, then the width of the bubble is scaled by the X axis scale and the height is scaled by the Y axis scale.
 
Table 9: ScatterChart Variables
Name
Type
Description
data
Series[]
The chart data series
dataEffect
Effect
Effect applied to all plotted lines and symbols
xAxis
ValueAxis
The X axis, which is along the bottom of the plot by default.
yAxis
ValueAxis
The Y axis, which is along the bottom of the plot by default.
 
Table 10: BubbleChart.Data Variables
Name
Type
Description
bubble
Node
The bubble node to display for this data item. Used for custom bubbles
radius
Number
The radius of the bubble. If scaleBubbleRadiusUsingAxis is true, then this is scaled by x and y axis scales.
xValue
Number
The data value to be plotted on the X axis
yValue
Number
The data value to be plotted on the Y axis
 

Expanding StockReader

A great way to enhance the StockReader application from the previous article would be to include graphs to visually represent the data being provided by the RSSTasks. Obviously, the graph would need to display more than just the current price. The user would some type of historic data on each stock. Therefore, the new version of StockReader locally stores the last price of each stock, then loads the stored prices to a variable in the model to be used by the graphs. Below is an image portraying the new StockInfoDialog.fx, which is opened by clicking on a StockItemNode (the squares of stocks on the main screen), and some enhanced snippets of code from StockReaderModel.fx.

 
Figure 7: StockInfoDialog
 
 
                   /**    * Stores the prices of the stocks from the last time the program was run, IF    * there are prices in local storage.    */   public var oldPrices:Number[];

  public function saveProperties():Void {
    println("Storage.list():{Storage.list()}");
    entry = Storage {
      source: "stockreader.properties"
    };
    var resource:Resource = entry.resource;
    var properties:Properties = new Properties();
    def symbolsTemp = for (si in stockItems) "{si.stockSymbol},";
  
                    def pricesTemp = for (si in stockItems) "{si.price.toString()},";
    println("symbolsTemp looks this this: {symbolsTemp}");
    properties.put("symbolsTemp", "{symbolsTemp}");
  
                    properties.put("pricesTemp", "{pricesTemp}");
    try {
      var outputStream:OutputStream = resource.openOutputStream(true);
      properties.store(outputStream);
      outputStream.close();
      println("properties written");
    }
    catch (ioe:IOException) {
      println("IOException in saveProperties:{ioe}");
    }
  };

  public function loadProperties():Void {
    println("Storage.list():{Storage.list()}");
    entry = Storage {
      source: "stockreader.properties"
    };
    var resource:Resource = entry.resource;
    var properties:Properties = new Properties();
    try {
      var inputStream:InputStream = resource.openInputStream();
      properties.load(inputStream);
      inputStream.close();
      def symbolsTemp = properties.get("symbolsTemp");
  
                      def pricesTemp = properties.get("pricesTemp");

      // If symbolsTemp was not empty, separate the commas and assign to symbols 
      if (symbolsTemp != null and symbolsTemp.trim() != "") {
        println("written symbols looks like this: {symbolsTemp}");
        symbols = symbolsTemp.split(",");
        println("symbols looks like this: {symbols}");
      }
      else {
        symbols = [];
      }

  
                      // If pricesTemp was not empty, separate the commas and assign to oldPrices       if (pricesTemp != null) {         println("written prices looks like this: {pricesTemp}");         def pricesToConvert = pricesTemp.split(",");         oldPrices = for (price in pricesToConvert) Number.parseFloat(price);         println("oldPrices looks like this: {oldPrices}");       }       else {         oldPrices = [];       }

      println("{sizeof symbols} properties read: {for (i in symbols) "-{i}-\n"}");
    }
    catch (ioe:IOException) {
      println("IOException in loadProperties:{ioe}");
    }
  };
       
              

Since each stock will need to provide its old prices for graphing the data, the StockItem.fx data model has been modified to include a new variable, var oldPrices:Number[]. Once the feeds have been started, the locally stored price will be placed into the StockItem's oldPrices sequence variable. As the feeds are refreshed and the StockItem's price is changed, an on replace trigger adds the old value of price to the oldPrices sequence, providing more data for the graph. Below is an excerpt from the modified startFeeds() function.

/**
   * A function that will run a feed for each stock symbol, and create a StockItem
   * data model for each.
   */
  public function startFeeds(
                 startup:Boolean):Void {
    for (si in stockItems) {
      println("symbol is {si.stockSymbol}");
      feedTask = RssTask {

        location: "http://www.quoterss.com/quote.php?symbol={si.stockSymbol}&frmt=0&Freq=0"
        interval: 60m

        onException: function(e) {
          println("Exception is: {e}");
            si.price = "{indexof si + 1}";
            si.time = "Feed Error";
            si.date = "Feed Error";
        }

        onChannel: function(channel) {
            println("{channel.title}");
        }

        onItem: function(item) {
            println("{item.title}");
            si.price = parseTitleString(item.title, si.stockSymbol)[0];
  
                            if (startup) {               insert oldPrices[indexof si] into si.oldPrices;             }
            si.time = parseTitleString(item.title, si.stockSymbol)[2];
            si.date = parseTitleString(item.title, si.stockSymbol)[3];
        }

        onDone: function():Void {
            feedTask.stop();
        }

      }
      feedTask.start();
    }
  };
       
              

Now that the old prices are being stored correctly, we need to create the node(s) that will display the graph and stock information. For this, we have created a custom class called StockInfoDialog.fx (shown in Figure 7:StockInfoDialog). The dialog will appear in the same manner as the AddStockDialog from the previous article, and will be visible once a user clicks on a stock on the main screen. The StockInfoDialog will have a reference to the StockItem data model that was chosen, and therefore have access to all data stored in price and oldPrices, as well as the stock symbol, and the time and date it was last changed. Take a look at the code sample below, which snippets of code pertaining to the creation of the Line Chart, and use of the StockItem data.

public class StockInfoDialog extends CustomNode {

  /**
   * A reference to the model
   */
  var model = StockReaderModel.getInstance();

  /**
   * The StockItem which was chosen
   */
  public var stockItem:StockItem;

  var background:Rectangle;

  /**
   * The background of the dialog
   */
  var infoBackground:Rectangle = Rectangle {

        . . . Some Code Omitted . . .

  };

  /**
* The Stack that will center the main background and infoBackground
*/
  var stack:Stack;

  /**
   * Contains the current stock's information
   */
  var currentStockInfo:VBox = VBox {
    layoutY: 25
    spacing: 10
    content: [
      Text {
        content: bind stockItem.stockSymbol
        font: Font.font("Arial", FontWeight.BOLD, 16)
        textOrigin: TextOrigin.TOP
        fill: Color.WHITE
      },
    
      . . . Some Code Omitted . . .

  };

  var lineChart:LineChart;

  . . . Some Code Omitted . . .

  public function createGraph():Void {
    lineChart = LineChart {
      title: "{stockItem.stockSymbol} History"
      titleFill: Color.WHITE
      //visible: bind selectedIndex == 3
      //showSymbols: false
      dataEffect: null
      xAxis: NumberAxis {
        lowerBound: 1
        upperBound: sizeof stockItem.oldPrices + 1
        tickUnit: 0
        labelFill: Color.WHITE
      }
      yAxis: NumberAxis {
        lowerBound: getLowerBound() - 25
        upperBound: getUpperBound() + 25
        tickUnit: 25
        labelFill: Color.WHITE
      }
      data: LineChart.Series {
        name: stockItem.stockSymbol
        data: [
          for (price in stockItem.oldPrices) {
            LineChart.Data {
              xValue: indexof price + 1
              yValue: stockItem.oldPrices[indexof price]
            }
          }
          LineChart.Data {
            xValue: sizeof stockItem.oldPrices + 1
            yValue: if (stockItem.price != "" or stockItem.price != null) Number.parseFloat(stockItem.price) else 0
          }
        ]
      }
    }
  };

  . . . Some Code Omitted . . .

  override function create():Node {
    stack = Stack {
      blocksMouse: true
      nodeHPos: HPos.CENTER
      nodeVPos: VPos.CENTER
      visible: bind dialogVisible
      width: bind backgroundWidth
      height: bind backgroundHeight
      content: [
        background = Rectangle {
          width: bind stack.width
          height: bind stack.height
          fill: Color.BLACK
          opacity: bind backgroundOpacity
        },
        infoGroup = Group {
          opacity: bind infoGroupOpacity
          content: [
            infoBackground,
            hbox = HBox {
              layoutX: 25
              layoutY: 25
              spacing: 25
              content: bind [
                currentStockInfo,
                lineChart
              ]
            },
            closeButton
          ]
        }
      ]
    }
  }
}
       

As a developer in JavaFX, I am drawn to the "bind" keyword, which is so frequently used to update the UI when data changes behind the scenes. As of yet, Sun is still working towards the ability to bind the chart nodes to data. Currently, binding will cause a compile error. However, notice that the preceding code provides a workaround for this issue. When the dialog is made visible throught the user's click to the stock node, we call a function createGraph that creates a new graph and assigns it to the lineChart variable. Because the scenegraph is bound, once the lineChart variable is changed, the chart in the scenegraph is updated.

This small addition of a graph can have a huge impact on the end-user's experience, allowing them to better visualize the historic data they wish to see. The StockReader application relies heavily on data, and because JavaFX charts require such little code, the integration of the Line Chart is a simple task.

Conclusion

Creating custom charts in JavaFX 1.2 is extremely simple and useful for many types of data-centric applications. Another facet of custom charts, which hasn't been discussed in detail in this article, is customization of the chart's appearance, another easy task. Each chart's API contains numerous variables for adding visual effects and altering styling for most every element. Be sure to play around with these variables and charts in your applications, and check out our next article. It covers RSS and Atom feeds in great detail with the development of Jim Weaver's SpeedReaderFX.

For More Information

Rate This Article

 
 

Discussion

We welcome your participation in our community. Please keep your comments civil and on point. You can optionally provide your email address to be notified of replies—your information is not used for any other purpose. By submitting a comment, you agree to these Terms of Use.