Articles
JavaFX Technologies
|
| 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 |
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:
/**
* 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.
|
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.
|
| |
||
|
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
|
| |
||
|
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.
|
| |
||
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).
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.
|
Name
|
Type
|
Description
|
|---|---|---|
| |
||
|
resource
|
Resource
|
The resource to be managed
|
|
source
|
String
|
The path (or absolute path) to the resource
|
| |
||
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.
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]
}
]
}
}
};
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.
|
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.
|
| |
||
|
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.
|
| |
||
|
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
|
| |
||
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.
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:
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.
|
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.
|
| |
||
|
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.
|
| |
||
|
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
|
| |
||
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.
/**
* 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.
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.
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 repliesyour information is not used for any other purpose. By submitting a comment, you agree to these Terms of Use.