Friday, June 12, 2009

Creating XYCharts in JavaFX

Note: This entry is the second in a series on the new chart components in JavaFX 1.2. Like the first entry, this one is also an excerpt from the upcoming "Pro JavaFX Platform" book written by Jim Weaver, Stephen Chin, Weiqi Gao, and myself. You can find out more by clicking on the book image to the right of this page. -- Dean The remaining five types of charts are all meant to work with XY data. These charts are all subtypes of the XYChart base class. As such they are rendered against a background grid and include a horizontal and vertical axis. Bar Chart A bar chart plots data from a sequence of BarChart.Series objects. Each series contains a sequence of BarChart.Data objects that each contains the value (the height of the bar) and the category that the bar belongs to. Therefore a bar chart data object can be declared as shown in the following code snippet.
BarChart.Data {
  category: “Category Name”
  value: 42
}
The horizontal axis for the bar chart is of type CategoryAxis. The public categories sequence variable must contain category names that match those given to the BarChart.Data objects. The vertical axis of a bar chart is a ValueAxis. As of JavaFX 1.2, the only concrete implementation of a ValueAxis is the NumberAxis class. A NumberAxis represents a range of numeric values and has variables that allow for control over the appearance and the number of tick marks and label that are shown along the axis. A typical axis declaration for a bar chart is shown in the listing below. The code declares a category axis with three categories and a number axis that ranges from 0 to 100 with a labeled tick mark every 20 units for a total of 6 labels (counting the label at 0).
BarChart {
  categoryAxis: CategoryAxis {
    categories: [ "Category 1", "Category 2", "Category 3" ]
  }
  valueAxis: NumberAxis {
    label: "Y Axis"
    upperBound: 100
    tickUnit: 20
  }
}
The next listing shows a complete program that displays a bar chart showing the sales report for Acme, Incorporated from 2007 to 2009. The data to be displayed in the chart is defined at the top. Each year will be it’s own category. The data values are generated from the sales figures for each of the three products over the three different years. The vertical axis is a NumberAxis that shows the number of units sold and is defined such that it can accommodate the largest sales value. It will have a labeled tick mark every 1,000 units starting at 0 and ending at 3,000. This gives a total of four labels along the vertical axis. The public data variable accepts the sequence of BarChart.Series objects to plot. Each BarChart.Series object also has its own public data variable, which accepts a sequence of BarChart.Data objects. A for expression is used to generate the actual sequence of BarChart.Data objects from the sequences defined at the beginning of the listing. Finally, the categoryGap variable (highlighted) is used to increase the spacing between the yearly data to provide more differentiation between the categories. The complete source code is in the BarChartIntro.fx file from the ChartIntro example project.
def years = [ "2007", "2008", "2009" ];
def anvilsSold = [  567, 1292, 2423 ];
def skatesSold = [  956, 1665, 2559 ];
def pillsSold = [ 1154, 1927, 2774 ];

Stage {
  title: "Bar Chart Intro"
  scene: Scene {
    content: [
      BarChart {
        title: "Acme, Inc. Sales Report"
        titleFont: Font { size: 24 }
        categoryGap: 25
        categoryAxis: CategoryAxis {
          categories: years
        }
        valueAxis: NumberAxis {
          label: "Units Sold"
          upperBound: 3000
          tickUnit: 1000
        }
        data: [
          BarChart.Series {
            name: "Anvils"
            data: for (j in [0..<sizeof years]) {
              BarChart.Data {
                category: years[j]
                value: anvilsSold[j]
              }
            }
          }
          BarChart.Series {
            name: "Rocket Skates"
            data: for (j in [0..<sizeof years]) {
              BarChart.Data {
                category: years[j]
                value: skatesSold[j]
              }
            }
          }
          BarChart.Series {
            name: "Earthquake Pills"
            data: for (j in [0..<sizeof years]) {
              BarChart.Data {
                category: years[j]
                value: pillsSold[j]
              }
            }
          }
        ]
      }
    ]
  }
}
Caution If you forget to declare the axes for a XYChart then no data will show up on your chart. This is one of the few times you cannot rely on the default values of the chart. In the previous example, if the CategoryAxis or the ValueAxis were left undeclared then no bars would have been drawn on the sales chart. The resulting chart is shown in below. Much like the pie charts, the default look for bar charts is a clean, modern look with lighting and shading affects built right in. There is also a drop-in replacement for BarChart named BarChart3D, which can be used to give the chart a 3-dimensional appearance. Line and Area Charts A line chart can be constructed using the LineChart class. It can show one or more LineChart.Series objects each of which contain a sequence of LineChart.Data objects. This pattern should be familiar now. It is the same for an AreaChart, whose data variable accepts a sequence of AreaChart.Series objects each of which contain a sequence of AreaChart.Data objects. We will use these charts to plot the mathematical functions sine and cosine as shown here: The source code for this program can be found in the LineAreaChartIntro.fx file in the ChartIntro example project. To plot the sine and cosine functions, we need to create a horizontal axis that goes from 0 to 2π and a vertical axis that goes from -1.0 to 1.0. The listing below shows two functions that generate these NumberAxis objects. You may be wondering why these axis objects are being generated by functions rather than just declaring them as variables and reusing them for both charts. The answer lies in the fact that the Axis base class is derived from Node. Like any Node, an Axis can only appear in the scene graph once. Since we are not allowed share these axis objects between our two charts, we must write functions that create new axis objects each time they are called.
/**
 * An x axis that goes from 0 to 2*PI with labels every PI/2 radians.
 * The labels are formatted to display on 2 significant digits.
 */
function createXAxis() {
  NumberAxis {
    label: "Radians"
    upperBound: 2 * Math.PI
    tickUnit: Math.PI / 2
    formatTickLabel: function(value) {
      "{%.2f value}"
    }
  }
}

/**
 * A y axis that that goes from -1 to 1 with labels every 0.5 units.
 */
function createYAxis() {
  NumberAxis {
    upperBound: 1.0
    lowerBound: -1.0
    tickUnit: 0.5
  }
}
The createXAxis function illustrates the use of the formatTickLabel function variable to format the values that will be used as tick labels on the axis. In this case, we format the numbers to keep only two significant digits after the decimal point. In addition, the code to create the y-axis shows how to set a non-zero lower bound for an axis. The next code listing shows the code that creates the LineChart object and adds it to the scene. This code follows the same pattern we’ve seen before. LineChart has a public data variable that takes a sequence of LineChart.Series objects. In this case we have one sequence for the sine wave and one for the cosine wave. Within each series, the data sequence is populated by a range expression that generates the LineChart.Data objects, which correspond to the points along the sine or cosine curves. Since we can’t show any data without our axes, we use the createXAxis and createYAxis functions shown earlier. Normally a line chart will plot a symbol at each data point – a circle, triangle, square or some such shape. The lines of the chart then connect these symbols. In this case, we have so many data points that the symbols would obscure the lines. So we tell the chart not to generate the symbols by setting the showSymbols variable to false. Another default setting for line charts is for the data to cast a shadow on the chart’s background. Since this makes the lines of this particular chart more difficult to see we turn off the drop shadow by setting dataEffect to null.
function createAreaChart() {
  AreaChart {
    title: "Area Chart"
    translateX: 550
    xAxis: createXAxis()
    yAxis: createYAxis()
    data: [
      AreaChart.Series {
        name: "Sine Wave"
        data: for (rads in [0..2*Math.PI step 0.01]) {
          AreaChart.Data {
            xValue: rads
            yValue: Math.sin( rads )
          }
        }
      }
      AreaChart.Series {
        name: "Cosine Wave"
        data: for (rads in [0..2*Math.PI step 0.01]) {
          AreaChart.Data {
            xValue: rads
            yValue: Math.cos( rads )
          }
        }
      }
    ]
  }
}
Scatter and Bubble Charts Scatter and bubble charts are just like the other three XY charts we’ve looked at. The classes, ScatterChart and BubbleChart respectively, have a public data variable that accepts a sequence of series object. You guessed it: ScatterChart.Series and BubbleChart.Series. Each of these series has a public data variable that holds a sequence of their respective data objects: ScatterChart.Data and BubbleChart.Data. However, the BubbleChart.Data class also contains a radius variable that is used to set the size of the bubble for each data point. The figure below shows an example of a scatter chart and a bubble chart. In this program, the charts are just plotting the distribution of points generated by the javafx.util.Math.random function. Using the JavaFX math functions allow our code to remain portable to mobile devices. We will discuss that more in Chapter 10. The radius of the bubbles in the bubble chart is determined by the order in which the points are generated. The bubbles start small and get bigger as points are generated. You can roughly tell the order in which each point was generated. This is simply an interesting way to view the randomness of the random number generator. The code for this program is found in ScatterBubbleChartIntro.fx in the ChartIntro example project. The code to create a scatter chart is pretty straightforward. First we define a function that creates a number axis that ranges from 0.00 to 1.00 with labels every 0.25 units. The tick labels are once again formatted to keep only 2 digits after the decimal points. The createAxis function itself takes a String parameter to use as the label for the number axis. This allows the reuse of the function to create labels for both the x- and y-axes. The ScatterChart has only one data series and that series contains a sequence of 100 data objects whose x and y values are generated randomly. The only extra bit of customization done here is to hide the legend since we do only have the one data series.
/**
 * An x axis that goes from 0 to 1.0 and displays labels every 0.25 units.
 * The labels are formatted to display on 2 significant digits.
 */
function createAxis( label:String ) {
  NumberAxis {
    label: label
    upperBound: 1.0
    tickUnit: 0.25
    formatTickLabel: function(value) {
      "{%.2f value}"
    }
  }
}

/**
 * Create a scatter chart that displays random points.
 */
function createScatterChart() {
  ScatterChart {
    title: "Scatter Chart"
    legendVisible: false
    xAxis: createAxis( "X Axis" )
    yAxis: createAxis( "Y Axis" )
    data: [
      ScatterChart.Series {
        data: for (i in [1..100]) {
          ScatterChart.Data {
            xValue: Math.random()
            yValue: Math.random()
          }
        }
      }
    ]
  }
}
The source code that creates the bubble chart, shown below, is very similar. The big difference here is the radius variable of the BubbleChart.Data class. This is unique to the bubble chart data and, as previously mentioned, allows us to control the size of the bubble on the plot. The radius values scale based on the axes of the plot. In our case the axes both go from 0.0 to 1.0. Therefore a bubble radius of 0.5 would create a bubble that filled the entire chart (if centered) since its diameter would be 1.0. If our axes went from 0 to 10 instead, then a bubble with a radius of 0.5 would be smaller, taking up only 1/10 of the chart. The code in the listing below creates bubbles whose radius varies from 0.001 to 0.1 as the value of i goes from 1 to 100.
function createBubbleChart() {
  BubbleChart {
    title: "Bubble Chart"
    legendVisible: false
    translateX: 550
    xAxis: createAxis( "X Axis" )
    yAxis: createAxis( "Y Axis" )
    data: [
      BubbleChart.Series {
        data: for (i in [1..100]) {
          BubbleChart.Data {
            xValue: Math.random()
            yValue: Math.random()
            radius: i / 1000.0
          }
        }
      }
    ]
  }
}
This concludes part two of this series. In the third and final excerpt, we will take a look at adding interaction to your charts and all of the customization options available in the chart API.

7 comments:

  1. FYI, in the second code snippet, you left out the property name for valueAxis.

    ReplyDelete
  2. Good catch, Les! Fixed now. Thanks!

    ReplyDelete
  3. Hi,

    Great example, thanks!
    How did you manage to get the two different grey colours in the graph background?
    I'm looking for a way to add dashed line and 3 different colours.

    Thanks, Birgit

    ReplyDelete
  4. can u please post sample code how to get grids in the background of a chart

    thanks

    ReplyDelete
  5. uday,

    I didn't do anything special to the chart backgrounds. All of the source code for those samples is available here:

    http://jfxtras.org/portal/pro-javafx-platform

    Just scroll down and look under the Chapter 5 samples.

    Dean

    ReplyDelete
  6. Is it possible to set the all chart width and height? I've been trying to set them but nothing happens!

    ReplyDelete
  7. Hi all,

    I am a developer gearing up to start exploring JavaFX.

    I have a web application using Struts with the front end being JSP.
    I am a newbie, and I wish to use JavaFX instead of the Jsp to create a better UI and provide it as a solution to my team mates.

    Can you kindly mail me a sample Struts application that uses JavaFX as the front end.

    My mail id:
    s.its.chandru@gmail.com

    I tried to browse through the net but in vain.

    Thanks for your help,
    Chandrasekar V.

    ReplyDelete

Please Note: All comments are moderated. That's why you won't see your comment appear right away. If it's not some stupid piece of spam, it will appear soon.

Note: Only a member of this blog may post a comment.