Home > Articles > Programming > Java

Java Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

Price Volume Chart

Last updated Mar 14, 2003.

Thus far, the stock graphs have been nice. However, one common time-based correlation that you make when analyzing stock prices, which we have missed, is relating the price to the daily volume. One difficult aspect of doing this is that, while a stock like MSFT may be around $30, the volumes are more in the range of 50K – 100K shares traded per day. So, to plot both values on the same graph, we need two different y-axes. Moreover, most stock graphs plot volume as a histogram behind a time series graph of prices.

Figure 53 shows a screenshot of a MSFT price/volume chart for October 2004.

Figure 53Figure 53. Price Volume Chart

To build this chart, we need to fulfill the following requirements:

  1. Create two separate y-axes

  2. Create a histogram and draw it behind the familiar time series of open/close prices

The key to accomplishing the first task is that once a time series JFreeChart is created, we obtain its Plot (in this case it is an XYPlot), create a new axis, and set the new axis as a range access for the chart. Recall that XY charts have two axes: the domain access governs the x-axis values and the range access governs the y-axis. So if we create a new axis, add it as an additional range axis, and map our volume time series to that axis, then we effectively have two concurrent y-axes.

For example:

    JFreeChart chart = ChartFactory.createTimeSeriesChart(
      title, 
      "Date", 
      "Price",
      priceDataset, 
      true,
      true,
      false
    );
    XYPlot plot = chart.getXYPlot();
    NumberAxis rangeAxis2 = new NumberAxis("Volume");
    plot.setRangeAxis( 1, rangeAxis2 );
    plot.setDataset( 1, volumeDataset );
    plot.mapDatasetToRangeAxis( 1, 1 );

A NumberAxis is a ValueAxis that gives us an axis that displays numerical data. There are other axes types that you might want to look at later, such as a LogarithmicAxis and ModuloAxis, which are used to display data whose values follow an non-linear pattern.

An interesting aspect of the XYPlot class is that it natively supports more than one (or even two) range axis: create a new axis and call its setRangeAxis() methodm passing it the 0-based index of the axis to use. Zero refers to the default axis, and in this example we added a second axis (index 1); we could create more axes and add them by passing other sequential indicies. Once we have a new axis defined then we add an additional dataset to the chart and finally map the new dataset (index 1) to the desired axis (index 1).

Our next task is to create a histogram rather than a line graph for the new axis. Recall that JFreeChart delegates the presentation of its data to individual renderers. Line graphs are drawn using the XYItemRenderer, while histograms are drawn using an XYBarRenderer.

So at a minimum this can be accomplished as follows:

XYBarRenderer renderer2 = new XYBarRenderer(0.20);
plot.setRenderer(1, renderer2);

Listing 4 puts all of this together into a method that will be added to the StockHistoryChart class.

Listing 4. buildPriceVolumeChart() method from StockHistoryChart class

  private JFreeChart buildPriceVolumeChart( TimeSeriesCollection priceDataset, 
                       TimeSeriesCollection volumeDataset, 
                       String title )
  {
    JFreeChart chart = ChartFactory.createTimeSeriesChart(
      title, 
      "Date", 
      "Price",
      priceDataset, 
      true,
      true,
      false
    );
    chart.setBackgroundPaint(Color.white);
    XYPlot plot = chart.getXYPlot();
    NumberAxis rangeAxis1 = (NumberAxis) plot.getRangeAxis();
    rangeAxis1.setLowerMargin(0.40); // to leave room for volume bars
    DecimalFormat format = new DecimalFormat("00.00");
    rangeAxis1.setNumberFormatOverride(format);

    XYItemRenderer renderer1 = plot.getRenderer();
    renderer1.setToolTipGenerator(
      new StandardXYToolTipGenerator(
        StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
        new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00")
      )
    );

    NumberAxis rangeAxis2 = new NumberAxis("Volume");
    rangeAxis2.setUpperMargin(1.00); // to leave room for price line
    plot.setRangeAxis( 1, rangeAxis2 );
    plot.setDataset( 1, volumeDataset );
    plot.mapDatasetToRangeAxis( 1, 1 );
    XYBarRenderer renderer2 = new XYBarRenderer(0.20);
    renderer2.setToolTipGenerator(
      new StandardXYToolTipGenerator(
        StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
        new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0,000.00")
      )
    );
    plot.setRenderer(1, renderer2);

    return chart;
  }

Most of listing 4 should be familiar to you. The rest contains formatting information for the axes (dates and values.)

Candlestick Chart

One chart that lends itself very well to displaying stock information is a candlestick chart. Candlestick charts display the opening and closing values of a stock, as well as its high and low for a given period. JFreeChart supports candlestick charts without much of any effort. Moreover, it overlays daily volumes at no cost to you (we had to do a lot to get that in the previous example!). Figure 54 shows a candlestick chart for MSFT for the same time period (October 2004).

Figure 54Figure 54. Candlestick Chart

Unfortunately for our demonstration purposes, MSFT did not have wild days in the provided month. But the way to read a candlestick chart is that the thin line represents the daily range (high and low) and the bar represents the open and close. If the bar is red, the stock went down that day; if it is green, the stock went up.

Listing 5 shows the source code for the entire buildCandlestickChart() method.

Listing 5. buildCandlestickChart() method from StockHistoryChart class

  private JFreeChart buildCandlestickChart( DefaultHighLowDataset dataset, String title )
  {
    JFreeChart chart = ChartFactory.createCandlestickChart(
      title,
      "Time", 
      "Value",
      dataset, 
      true
    );
    chart.setBackgroundPaint(Color.white);
    return chart;
  }

The ChartFactory class provides a createCandlestickChart() method that does all of the work for us, but the key is the construction of a DefaultHighLowDataset class. Although this class is easy to construct, it breaks our pattern of using collections of TimeSeries instances. Rather, it uses Java arrays:

DefaultHighLowDataset(java.lang.String seriesName, 
           java.util.Date[] date, 
           double[] high, 
           double[] low, 
           double[] open, 
           double[] close, 
           double[] volume)

Furthermore, instead of using the JFreeChart Day class, it falls back to using the java.util.Date class. This is not a huge problem, but to build all of these graphs in the same application we need to do a little conversion (from a TimeSeries to a double[]). Listing 6 shows the complete source code for the StockHistoryChart class.

Listing 6. StockHistoryChart.java

package com.javasrc.charts;

// Import the Java classes
import java.util.*;
import java.text.*;
import java.io.*;

// Import the Swing classes
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

// Import the JFreeChart classes
import org.jfree.chart.*;
import org.jfree.chart.axis.*;
import org.jfree.chart.plot.*;
import org.jfree.chart.renderer.*;
import org.jfree.chart.renderer.xy.*;
import org.jfree.data.*;
import org.jfree.data.general.*;
import org.jfree.data.time.*;
import org.jfree.ui.*;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.data.xy.*;

public class StockHistoryChart extends JPanel
{
  // Holds the data
  private TimeSeriesCollection dataset = new TimeSeriesCollection();
  private TimeSeriesCollection datasetOpenClose = new TimeSeriesCollection();
  private TimeSeriesCollection datasetHighLow = new TimeSeriesCollection();
  private TimeSeriesCollection datasetVolume = new TimeSeriesCollection();

  // Create a chart
  private JFreeChart chart;

  // Create a panels that can show our chart
  private ChartPanel panel;

  private String stockSymbol;

  public StockHistoryChart( String filename )
  {
    try
    {
      // Get Stock Symbol
      this.stockSymbol = filename.substring( 0, filename.indexOf( '.' ) );

      // Create time series
      TimeSeries open = new TimeSeries( "Open Price", Day.class );
      TimeSeries close = new TimeSeries( "Close Price", Day.class );
      TimeSeries high = new TimeSeries( "High", Day.class );
      TimeSeries low = new TimeSeries( "Low", Day.class );
      TimeSeries volume = new TimeSeries( "Volume", Day.class );

      BufferedReader br = new BufferedReader( new FileReader( filename ) );
      String key = br.readLine();
      System.out.println( "Key: " + key );
      ArrayList dates = new ArrayList();
      String line = br.readLine();
      while( line != null &&
          !line.startsWith( "<!--" ) )
      {
        StringTokenizer st = new StringTokenizer( line, ",", false );
        Day day = getDay( st.nextToken() );
        double openValue = Double.parseDouble( st.nextToken() );
        double highValue = Double.parseDouble( st.nextToken() );
        double lowValue = Double.parseDouble( st.nextToken() );
        double closeValue = Double.parseDouble( st.nextToken() );
        double volumeValue = Double.parseDouble( st.nextToken() );

        // Add this value to our series'
        open.add( day, openValue );
        close.add( day, closeValue );
        high.add( day, highValue );
        low.add( day, lowValue );
        volume.add( day, volumeValue );

        // Copy Day to Date
        java.util.Date date = new java.util.Date( day.getFirstMillisecond() );
        dates.add( date );

        // Read the next day
        line = br.readLine();
      }
      
      // Build the datasets
      dataset.addSeries( open );
      dataset.addSeries( close );
      dataset.addSeries( low );
      dataset.addSeries( high );
      datasetOpenClose.addSeries( open );
      datasetOpenClose.addSeries( close );
      datasetHighLow.addSeries( high );
      datasetHighLow.addSeries( low );
      datasetVolume.addSeries( volume );
      

      JFreeChart summaryChart = buildChart( dataset, "Summary", true );
      JFreeChart openCloseChart = buildChart( datasetOpenClose, "Open/Close Data", false );
      JFreeChart highLowChart = buildChart( datasetHighLow, "High/Low Data", true );
      JFreeChart highLowDifChart = buildDifferenceChart( datasetHighLow, "High/Low Difference Chart" );
      JFreeChart priceVolumeChart = buildPriceVolumeChart( datasetOpenClose, datasetVolume, "Price to Volume Chart" );

      // Build the Candlestick chart
      int itemCount = open.getItemCount();
      double[] openArray = new double[ itemCount ];
      double[] closeArray = new double[ itemCount ];
      double[] highArray = new double[ itemCount ];
      double[] lowArray = new double[ itemCount ];
      double[] volumeArray = new double[ itemCount ];
      java.util.Date[] dateArray = new java.util.Date[ itemCount ];
      for( int i=0; i<dates.size(); i++ )
      {
        dateArray[ i ] = ( java.util.Date )dates.get( i );
      }
      copyTimeSeriesToArray( openArray, open );
      copyTimeSeriesToArray( closeArray, close );
      copyTimeSeriesToArray( highArray, high );
      copyTimeSeriesToArray( lowArray, low );
      copyTimeSeriesToArray( volumeArray, volume );
      DefaultHighLowDataset highLowDataset = new DefaultHighLowDataset( "Stock Quotes", 
                                       dateArray, 
                                       highArray, 
                                       lowArray, 
                                       openArray, 
                                       closeArray, 
                                       volumeArray );
      JFreeChart candlestickChart = this.buildCandlestickChart( highLowDataset, "Stock Quotes" );

      // Create this panel
      this.setLayout( new GridLayout( 2, 3 ) );
      this.add( new ChartPanel( summaryChart ) );
      this.add( new ChartPanel( openCloseChart ) );
      this.add( new ChartPanel( highLowChart ) );
      this.add( new ChartPanel( highLowDifChart ) );
      this.add( new ChartPanel( priceVolumeChart ) );
      this.add( new ChartPanel( candlestickChart ) );
    }
    catch( Exception e )
    {
      e.printStackTrace();
    }
  }

  private void copyTimeSeriesToArray( double[] arr, TimeSeries series )
  {
    for( int i=0; i<series.getItemCount(); i++ ) 
    {
      Number num = series.getValue( i );
      arr[ i ] = num.doubleValue();
    }
  }

  private JFreeChart buildChart( TimeSeriesCollection dataset, String title, boolean endPoints )
  {
    // Create the chart
    JFreeChart chart = ChartFactory.createTimeSeriesChart(
      title,
      "Date", "Price",
      dataset,
      true,
      true,
      false
    );


    // Display each series in the chart with its point shape in the legend
    StandardLegend sl = (StandardLegend) chart.getLegend();
    sl.setDisplaySeriesShapes(true);

    // Setup the appearance of the chart
    chart.setBackgroundPaint(Color.white);
    XYPlot plot = chart.getXYPlot();
    plot.setBackgroundPaint(Color.lightGray);
    plot.setDomainGridlinePaint(Color.white);
    plot.setRangeGridlinePaint(Color.white);
    plot.setAxisOffset(new Spacer(Spacer.ABSOLUTE, 5.0, 5.0, 5.0, 5.0));
    plot.setDomainCrosshairVisible(true);
    plot.setRangeCrosshairVisible(true);

    // Display data points or just the lines?
    if( endPoints ) 
    {
      XYItemRenderer renderer = plot.getRenderer();
      if (renderer instanceof StandardXYItemRenderer) {
        StandardXYItemRenderer rr = (StandardXYItemRenderer) renderer;
        rr.setPlotShapes(true);
        rr.setShapesFilled(true);
        rr.setItemLabelsVisible(true);
      }
    }

    // Tell the chart how we would like dates to read
    DateAxis axis = (DateAxis) plot.getDomainAxis();
    axis.setDateFormatOverride(new SimpleDateFormat("MMM-yyyy"));
    return chart;
  }

  private JFreeChart buildDifferenceChart( TimeSeriesCollection dataset, String title )
  {
    // Creat the chart
    JFreeChart chart = ChartFactory.createTimeSeriesChart(
      title,
      "Date", "Price",
      dataset,
      true, // legend
      true, // tool tips
      false // URLs
    );
    chart.setBackgroundPaint(Color.white);
    
    XYPlot plot = chart.getXYPlot();
    plot.setRenderer(new XYDifferenceRenderer( new Color( 112, 128, 222 ), new Color( 112, 128, 222 ), false));
    plot.setBackgroundPaint(Color.white);
    plot.setDomainGridlinePaint(Color.white);
    plot.setRangeGridlinePaint(Color.white);
    plot.setAxisOffset(new Spacer(Spacer.ABSOLUTE, 5.0, 5.0, 5.0, 5.0));
    
    ValueAxis domainAxis = new DateAxis("Date");
    domainAxis.setLowerMargin(0.0);
    domainAxis.setUpperMargin(0.0);
    plot.setDomainAxis(domainAxis);
    plot.setForegroundAlpha(0.5f);

    return chart;

  }

  private JFreeChart buildPriceVolumeChart( TimeSeriesCollection priceDataset, 
                       TimeSeriesCollection volumeDataset, 
                       String title )
  {
    JFreeChart chart = ChartFactory.createTimeSeriesChart(
      title, 
      "Date", 
      "Price",
      priceDataset, 
      true,
      true,
      false
    );
    chart.setBackgroundPaint(Color.white);
    XYPlot plot = chart.getXYPlot();
    NumberAxis rangeAxis1 = (NumberAxis) plot.getRangeAxis();
    rangeAxis1.setLowerMargin(0.40); // to leave room for volume bars
    DecimalFormat format = new DecimalFormat("00.00");
    rangeAxis1.setNumberFormatOverride(format);

    XYItemRenderer renderer1 = plot.getRenderer();
    renderer1.setToolTipGenerator(
      new StandardXYToolTipGenerator(
        StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
        new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00")
      )
    );

    NumberAxis rangeAxis2 = new NumberAxis("Volume");
    rangeAxis2.setUpperMargin(1.00); // to leave room for price line
    plot.setRangeAxis( 1, rangeAxis2 );
    plot.setDataset( 1, volumeDataset );
    plot.mapDatasetToRangeAxis( 1, 1 );
    XYBarRenderer renderer2 = new XYBarRenderer(0.20);
    renderer2.setToolTipGenerator(
      new StandardXYToolTipGenerator(
        StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
        new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0,000.00")
      )
    );
    plot.setRenderer(1, renderer2);

    return chart;
  }

  private JFreeChart buildCandlestickChart( DefaultHighLowDataset dataset, String title )
  {
    JFreeChart chart = ChartFactory.createCandlestickChart(
      title,
      "Time", 
      "Value",
      dataset, 
      true
    );
    chart.setBackgroundPaint(Color.white);
    return chart;
  }

  protected Day getDay( String date )
  {
    try
    {
      StringTokenizer st = new StringTokenizer( date, "-", false );

      // Get the day
      int day = Integer.parseInt( st.nextToken() );

      // Get the month
      String monthStr = st.nextToken();
      int month = -1;
      if( monthStr.equalsIgnoreCase( "Jan" ) ) month = 1;
      else if( monthStr.equalsIgnoreCase( "Feb" ) ) month = 2;
      else if( monthStr.equalsIgnoreCase( "Mar" ) ) month = 3;
      else if( monthStr.equalsIgnoreCase( "Apr" ) ) month = 4;
      else if( monthStr.equalsIgnoreCase( "May" ) ) month = 5;
      else if( monthStr.equalsIgnoreCase( "Jun" ) ) month = 6;
      else if( monthStr.equalsIgnoreCase( "Jul" ) ) month = 7;
      else if( monthStr.equalsIgnoreCase( "Aug" ) ) month = 8; 
      else if( monthStr.equalsIgnoreCase( "Sep" ) ) month = 9; 
      else if( monthStr.equalsIgnoreCase( "Oct" ) ) month = 10; 
      else if( monthStr.equalsIgnoreCase( "Nov" ) ) month = 11; 
      else if( monthStr.equalsIgnoreCase( "Dec" ) ) month = 12; 

      // Get the year
      int year = Integer.parseInt( st.nextToken() );
      if( year >=0 && year <= 10 )
      {
        year += 2000;
      }
      else
      {
        year += 1900;
      }

      // Build a new Day
      return new Day( day, month, year );
    }
    catch( Exception e )
    {
      e.printStackTrace();
    }
    return null;
  }

  public String getSymbol()
  {
    return this.stockSymbol;
  }

  public static void main( String[] args )
  {
    if( args.length < 1 )
    {
      System.out.println( "Usage: StockHistoryChart filename.csv" );
      System.exit( 0 );
    }
    StockHistoryChart shc = new StockHistoryChart( args[ 0 ] );

    JFrame frame = new JFrame( "Stock History Chart for " + shc.getSymbol() );
    frame.getContentPane().add( shc, BorderLayout.CENTER );
    frame.setSize( 640, 480 );
    frame.setVisible( true );
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
  }
}

Summary

Charts and graphs can make your applications look better and better convey certain concepts to your users. Building a charting library would most probably take you longer than it's worth, so it is better to leverage the hard work of other individuals. In this case, JFreeChart provides a robust library that meets most charting needs.

In the first article, we looked at the architecture of building a JFreeChart application and then looked to pie charts. In the second article, we built time series and difference charts and finished up with a discussion of saving charts as JPEG files. This week, we built two charts that helped us display stock information: a price volume chart and a candlestick chart. In future articles we will look at more chart types as well as generating runtime graphs.