Search code examples
javaswingpluginsjfreechartjstockchart

jstockchart adding a third display


I have implemented jstockchart as a plugin to jfreechart.

I have altered their JStockChartGettingStarted by implementing the Yahoo Finance API to grab stock quotes.

I run the following specs:

  • JDK 1.7
  • commons-io-2.4
  • commons-lang3-3.1
  • jfreechart 1.0.15
  • jstockchart 0.4.3

I also use all packages found in the jstockchart 0.4.3 package.

Now my result looks as follows:

enter image description here

Now what I like so much about this plugin is that the volume and the price are seperated but the two displays are still linked. So if I zoom in one display, the other display also zooms.

I was wondering how I am able to add another display below the current two displays, which also interacts with the other two as I explained above.

I know the used plot is a combinedDomainXYPlot and that I can simply add to it as follows:

    if (timeseriesArea.getVolumeWeight() > 0) {
        XYPlot volumePlot = createVolumePlot();
        combinedDomainXYPlot.add(volumePlot, timeseriesArea
                .getVolumeWeight());
    }

But how do I add another display?

Update

So what I want to do is add an additional set of axes/an additional panel like visualised below:

enter image description here

So I know how to add additional plots to any panel but not how to add an extra (third) panel to the whole. Where this third panel also uses the same CombinedDomainXYPlot as the first two panels.

I know the code for adding an additional panel should be in the code below somewhere. But where?

Code for rendering the plots

package org.jstockchart.plot;

import java.awt.BasicStroke;

import org.jfree.chart.axis.SegmentedTimeline;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.time.TimeSeriesCollection;
import org.jstockchart.area.PriceArea;
import org.jstockchart.area.TimeseriesArea;
import org.jstockchart.area.VolumeArea;
import org.jstockchart.axis.TimeseriesDateAxis;
import org.jstockchart.axis.TimeseriesNumberAxis;
import org.jstockchart.axis.logic.CentralValueAxis;
import org.jstockchart.axis.logic.LogicDateAxis;
import org.jstockchart.axis.logic.LogicNumberAxis;
import org.jstockchart.dataset.TimeseriesDataset;

/**
 * Creates <code>CombinedDomainXYPlot</code> and <code>XYPlot</code> for the
 * timeseries chart.
 * 
 * @author Sha Jiang
 */
public class TimeseriesPlot {

    private static final long serialVersionUID = 8799771872991017065L;

    private TimeseriesDataset dataset = null;

    private SegmentedTimeline timeline = null;

    private TimeseriesArea timeseriesArea = null;

    /**
     * Creates a new <code>TimeseriesPlot</code> instance.
     * 
     * @param dataset
     *            timeseries data set(<code>null</code> not permitted).
     * @param timeline
     *            a "segmented" timeline.
     * @param timeseriesArea
     *            timeseries area.
     */
    public TimeseriesPlot(TimeseriesDataset dataset,
            SegmentedTimeline timeline, TimeseriesArea timeseriesArea) {
        if (dataset == null) {
            throw new IllegalArgumentException("Null 'dataset' argument.");
        }
        this.dataset = dataset;

        this.timeline = timeline;

        if (timeseriesArea == null) {
            throw new IllegalArgumentException(
                    "Null 'timeseriesArea' argument.");
        }
        this.timeseriesArea = timeseriesArea;
    }

    private CombinedDomainXYPlot createCombinedXYPlot() {
        LogicDateAxis logicDateAxis = timeseriesArea.getlogicDateAxis();
        TimeseriesDateAxis dateAxis = new TimeseriesDateAxis(logicDateAxis
                .getLogicTicks());
        if (timeline != null) {
            dateAxis.setTimeline(timeline);
        }

        CombinedDomainXYPlot combinedDomainXYPlot = new CombinedDomainXYPlot(
                dateAxis);
        combinedDomainXYPlot.setGap(timeseriesArea.getGap());
        combinedDomainXYPlot.setOrientation(timeseriesArea.getOrientation());
        combinedDomainXYPlot.setDomainAxis(dateAxis);
        combinedDomainXYPlot.setDomainAxisLocation(timeseriesArea
                .getDateAxisLocation());
        combinedDomainXYPlot.setDomainPannable(true);
        combinedDomainXYPlot.setRangePannable(true);

        if (timeseriesArea.getPriceWeight() <= 0
                && timeseriesArea.getVolumeWeight() <= 0) {
            throw new IllegalArgumentException(
                    "Illegal weight value: priceWeight="
                            + timeseriesArea.getPriceWeight()
                            + ", volumeWeight="
                            + timeseriesArea.getVolumeWeight());
        }

        if (timeseriesArea.getPriceWeight() > 0) {
            XYPlot pricePlot = createPricePlot();
            combinedDomainXYPlot
                    .add(pricePlot, timeseriesArea.getPriceWeight());
        }

        if (timeseriesArea.getVolumeWeight() > 0) {
            XYPlot volumePlot = createVolumePlot();
            combinedDomainXYPlot.add(volumePlot, timeseriesArea
                    .getVolumeWeight());
        }

        return combinedDomainXYPlot;
    }

    private XYPlot createPricePlot() {
        PriceArea priceArea = timeseriesArea.getPriceArea();
        TimeSeriesCollection priceDataset = new TimeSeriesCollection();
        priceDataset.addSeries(dataset.getPriceTimeSeries().getTimeSeries());
        if (priceArea.isAverageVisible()) {
            priceDataset.addSeries(dataset.getAverageTimeSeries()
                    .getTimeSeries());
        }

        CentralValueAxis logicPriceAxis = priceArea.getLogicPriceAxis();
        TimeseriesNumberAxis priceAxis = new TimeseriesNumberAxis(
                logicPriceAxis.getLogicTicks());
        XYLineAndShapeRenderer priceRenderer = new XYLineAndShapeRenderer(true,
                false);
        priceAxis.setUpperBound(logicPriceAxis.getUpperBound());
        priceAxis.setLowerBound(logicPriceAxis.getLowerBound());
        priceRenderer.setSeriesPaint(0, priceArea.getPriceColor());
        priceRenderer.setSeriesPaint(1, priceArea.getAverageColor());

        TimeseriesNumberAxis rateAxis = new TimeseriesNumberAxis(logicPriceAxis
                .getRatelogicTicks());
        rateAxis.setUpperBound(logicPriceAxis.getUpperBound());
        rateAxis.setLowerBound(logicPriceAxis.getLowerBound());

        XYPlot plot = new XYPlot(priceDataset, null, priceAxis, priceRenderer);
        plot.setBackgroundPaint(priceArea.getBackgroudColor());
        plot.setOrientation(priceArea.getOrientation());
        plot.setRangeAxisLocation(priceArea.getPriceAxisLocation());

        if (priceArea.isRateVisible()) {
            plot.setRangeAxis(1, rateAxis);
            plot.setRangeAxisLocation(1, priceArea.getRateAxisLocation());
            plot.setDataset(1, null);
            plot.mapDatasetToRangeAxis(1, 1);
        }

        if (priceArea.isMarkCentralValue()) {
            Number centralPrice = logicPriceAxis.getCentralValue();
            if (centralPrice != null) {
                plot.addRangeMarker(new ValueMarker(centralPrice.doubleValue(),
                        priceArea.getCentralPriceColor(), new BasicStroke()));
            }
        }
        return plot;
    }

    private XYPlot createVolumePlot() {
        VolumeArea volumeArea = timeseriesArea.getVolumeArea();
        LogicNumberAxis logicVolumeAxis = volumeArea.getLogicVolumeAxis();

        TimeseriesNumberAxis volumeAxis = new TimeseriesNumberAxis(
                logicVolumeAxis.getLogicTicks());
        volumeAxis.setUpperBound(logicVolumeAxis.getUpperBound());
        volumeAxis.setLowerBound(logicVolumeAxis.getLowerBound());
        volumeAxis.setAutoRangeIncludesZero(false);
        XYBarRenderer volumeRenderer = new XYBarRenderer();
        volumeRenderer.setSeriesPaint(0, volumeArea.getVolumeColor());
        volumeRenderer.setShadowVisible(false);

        XYPlot plot = new XYPlot(new TimeSeriesCollection(dataset
                .getVolumeTimeSeries()), null, volumeAxis, volumeRenderer);
        plot.setBackgroundPaint(volumeArea.getBackgroudColor());
        plot.setOrientation(volumeArea.getOrientation());
        plot.setRangeAxisLocation(volumeArea.getVolumeAxisLocation());
        return plot;
    }

    public CombinedDomainXYPlot getTimeseriesPlot() {
        return createCombinedXYPlot();
    }

    public TimeseriesDataset getDataset() {
        return dataset;
    }

    public void setDataset(TimeseriesDataset dataset) {
        if (dataset == null) {
            throw new IllegalArgumentException("Null 'dataset' argument.");
        }
        this.dataset = dataset;
    }

    public SegmentedTimeline getTimeline() {
        return timeline;
    }

    public void setTimeline(SegmentedTimeline timeline) {
        this.timeline = timeline;
    }

    public TimeseriesArea getTimeseriesArea() {
        return timeseriesArea;
    }

    public void setTimeseriesArea(TimeseriesArea timeseriesArea) {
        if (timeseriesArea == null) {
            throw new IllegalArgumentException(
                    "Null 'timeseriesArea' argument.");
        }
        this.timeseriesArea = timeseriesArea;
    }
}

The code for the frontpanel display is as follows:

Code for GUI

package gui;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;

import javax.swing.JFrame;

import org.jfree.chart.ChartPanel;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.SegmentedTimeline;
import org.jfree.data.Range;
import org.jfree.data.time.Minute;
import org.jfree.data.xy.OHLCDataItem;
import org.jstockchart.JStockChartFactory;
import org.jstockchart.area.PriceArea;
import org.jstockchart.area.TimeseriesArea;
import org.jstockchart.area.VolumeArea;
import org.jstockchart.axis.TickAlignment;
import org.jstockchart.axis.logic.CentralValueAxis;
import org.jstockchart.axis.logic.LogicDateAxis;
import org.jstockchart.axis.logic.LogicNumberAxis;
import org.jstockchart.dataset.TimeseriesDataset;
import org.jstockchart.model.TimeseriesItem;
import org.jstockchart.util.DateUtils;

/**
 * Demo application for JStockChart timeseries.
 * 
 * @author Sha Jiang
 */
public class TimeseriesChartDemo {

    public static int period = 400;

    public static void main(String[] args) throws IOException {
        String imageDir = "./images";
        File images = new File(imageDir);
        if (!images.exists()) {
            images.mkdir();
        }
        String imageFile = imageDir + "/jstockchart-timeseries.png";


        Date startTime = DateUtils.createDate(2008, 1, 1, 9, 30, 0);
        Date endTime = DateUtils.createDate(2008, 1, 1, 15, 0, 0);
        // 'data' is a list of TimeseriesItem instances.
        List<TimeseriesItem> data = getData("AAPL", period, "d");

        // the 'timeline' indicates the segmented time range '00:00-11:30, 13:00-24:00'.
        SegmentedTimeline timeline = new SegmentedTimeline(
                SegmentedTimeline.DAY_SEGMENT_SIZE, 1351, 89);
        timeline.setStartTime(SegmentedTimeline.firstMondayAfter1900() + 780
                * SegmentedTimeline.DAY_SEGMENT_SIZE);

        // Creates timeseries data set.
        TimeseriesDataset dataset = new TimeseriesDataset(Minute.class, 1,
                timeline, true);
        dataset.addDataItems(data);

        DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(Locale.US);
        otherSymbols.setDecimalSeparator('.');
        otherSymbols.setGroupingSeparator(','); 
        DecimalFormat df = new DecimalFormat(".##", otherSymbols);

        // Creates logic price axis.
        CentralValueAxis logicPriceAxis = new CentralValueAxis(
                dataset.getPriceTimeSeries().getTimeSeries().getValue(data.size()-1).doubleValue(), new Range(
                        dataset.getMinPrice().doubleValue(), dataset
                                .getMaxPrice().doubleValue()), 9,
                                df);
        PriceArea priceArea = new PriceArea(logicPriceAxis);

        // Creates logic volume axis.
        LogicNumberAxis logicVolumeAxis = new LogicNumberAxis(new Range(dataset
                .getMinVolume().doubleValue(), dataset.getMaxVolume()
                .doubleValue()), 5, new DecimalFormat("0"));
        VolumeArea volumeArea = new VolumeArea(logicVolumeAxis);

        TimeseriesArea timeseriesArea = new TimeseriesArea(priceArea,
                volumeArea, createlogicDateAxis(DateUtils
                        .createDate(2008, 1, 1)));

        JFreeChart jfreechart = JStockChartFactory.createTimeseriesChart(
                "Stock chart test with two seperate displays", dataset, timeline, timeseriesArea,
                false);


        JFrame outside = new JFrame();
        ChartPanel chartPanel = new ChartPanel(jfreechart, false);

        chartPanel.setMouseWheelEnabled(true);

        outside.add(chartPanel);

        outside.setVisible(true);

        ChartUtilities
                .saveChartAsPNG(new File(imageFile), jfreechart, 545, 300);
    }

    // Specifies date axis ticks.
    private static LogicDateAxis createlogicDateAxis(Date baseDate) {
        LogicDateAxis logicDateAxis = new LogicDateAxis(baseDate,
                new SimpleDateFormat("HH:mm"));
        logicDateAxis.addDateTick("09:30", TickAlignment.START);
        logicDateAxis.addDateTick("10:00");
        logicDateAxis.addDateTick("10:30");
        logicDateAxis.addDateTick("11:00");
        logicDateAxis.addDateTick("11:30", TickAlignment.END);
        logicDateAxis.addDateTick("13:00", TickAlignment.START);
        logicDateAxis.addDateTick("13:30");
        logicDateAxis.addDateTick("14:00");
        logicDateAxis.addDateTick("14:30", TickAlignment.END);
        logicDateAxis.addDateTick("15:00", TickAlignment.END);
        return logicDateAxis;
    }

    static List<TimeseriesItem> dataItems;

    static boolean TodayAdded = true;

    static ArrayList<Double> prices;
    static ArrayList<Date> dates;

    static List<TimeseriesItem> getData(String stockSymbol, int periodToLoad, String periodUnit) {

        TodayAdded = true;

        dataItems = new ArrayList<TimeseriesItem>();

        Date today = new Date();
        today = addDays(today, 1);
        Date beginDate = addDays(today, -periodToLoad);

        GregorianCalendar BEGIN = (GregorianCalendar) DateToCalendar(beginDate);
        GregorianCalendar END   = (GregorianCalendar) DateToCalendar(today);

        String QUOTE = constructURL(stockSymbol, BEGIN, END, periodUnit);

        try {
            String strUrl = QUOTE;
            URL url = new URL(strUrl);
            BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
            DateFormat df = new SimpleDateFormat("y-M-d");

            dates = new ArrayList<Date>();
            prices = new ArrayList<Double>();

            String inputLine;
            in.readLine();
            int counter = 0;

            while ((inputLine = in.readLine()) != null) {
                StringTokenizer st = new StringTokenizer(inputLine, ",");

                Date date       = df.parse( st.nextToken() );
                double open     = Double.parseDouble( st.nextToken() );
                double high     = Double.parseDouble( st.nextToken() );
                double low      = Double.parseDouble( st.nextToken() );
                double close    = Double.parseDouble( st.nextToken() );
                double volume   = Double.parseDouble( st.nextToken() );
                double adjClose = Double.parseDouble( st.nextToken() );

                double price = close;

                dataItems.add(new TimeseriesItem(date, close, volume));

                System.out.println(close);

                dates.add(date);
                prices.add(close);
            }
            in.close();
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        //Reversal of dates
        Collections.reverse(dates);
        Collections.reverse(prices);
        //Data from Yahoo is from newest to oldest. Reverse so it is oldest to newest


        return dataItems;
    }

    public static Date addDays(Date date, int days)
    {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.add(Calendar.DATE, days); //minus number would decrement the days
        return cal.getTime();
    }

    public static String constructURL(String symbol, Calendar start, Calendar end, String periodUnit) {
        return "http://ichart.finance.yahoo.com/table.csv" +
    "?s=" +
                symbol + 
    "&a=" +
                Integer.toString(start.get(Calendar.MONTH)) +
    "&b=" +
                start.get(Calendar.DAY_OF_MONTH) +
    "&c=" +
                Integer.toString(start.get(Calendar.YEAR)) +
    "&d=" +
                Integer.toString(end.get(Calendar.MONTH)) +
    "&e=" +
                Integer.toString(end.get(Calendar.DAY_OF_MONTH)) +
    "&f=" +
                Integer.toString(end.get(Calendar.YEAR)) +
    "&g=" +
                periodUnit +
    "&ignore=.csv";
    }

    public static Calendar DateToCalendar(Date date){ 
          Calendar cal = Calendar.getInstance();
          cal.setTime(date);
          return cal;
        }
}

Hope someone can help me out. Thanks in advance.


Solution

  • trashgod was right again. He truly is the god of jfreechart.

    I found out that the CombinedDomainXYPlot in the jstockchart package is built from a package called org.jstockchart.axis.logic which ensures every new plot gets its own frame.

    Or in other words, the package behaves as follows:

    • Create a (complete) stand-alone graph
    • Add graph to the CombinedDomainXYPlot
    • The package will make sure the graph gets its own panel

    In coding terms, this means that the following statement:

        if (timeseriesArea.getPriceWeight() > 0) {
            XYPlot pricePlot = createPricePlot();
            combinedDomainXYPlot
                    .add(pricePlot, timeseriesArea.getPriceWeight());
        }
    
        if (timeseriesArea.getVolumeWeight() > 0) {
            XYPlot volumePlot = createVolumePlot();
            combinedDomainXYPlot.add(volumePlot, timeseriesArea
                    .getVolumeWeight());
        }
    
        if (timeseriesArea.getPriceWeight() > 0) {
            XYPlot pricePlot2 = createPricePlot();
            combinedDomainXYPlot
                    .add(pricePlot2, timeseriesArea.getPriceWeight());
        }
    

    Creates the following output:

    enter image description here

    Which is exactly what I wanted to achieve (or wanted to show).

    So thank you very much trashgod, I didn't know the jstockchart package worked this way.