edu.jhuapl.graphs.jfreechart.JFreeChartTimeSeriesGraphSource.java Source code

Java tutorial

Introduction

Here is the source code for edu.jhuapl.graphs.jfreechart.JFreeChartTimeSeriesGraphSource.java

Source

/*
 * Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory
 *                             All rights reserved.
 *
 * This material may be used, modified, or reproduced by or for the U.S.
 * Government pursuant to the rights granted under the clauses at
 * DFARS 252.227-7013/7014 or FAR 52.227-14.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * NO WARRANTY.   THIS MATERIAL IS PROVIDED "AS IS."  JHU/APL DISCLAIMS ALL
 * WARRANTIES IN THE MATERIAL, WHETHER EXPRESS OR IMPLIED, INCLUDING (BUT NOT
 * LIMITED TO) ANY AND ALL IMPLIED WARRANTIES OF PERFORMANCE,
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT OF
 * INTELLECTUAL PROPERTY RIGHTS. ANY USER OF THE MATERIAL ASSUMES THE ENTIRE
 * RISK AND LIABILITY FOR USING THE MATERIAL.  IN NO EVENT SHALL JHU/APL BE
 * LIABLE TO ANY USER OF THE MATERIAL FOR ANY ACTUAL, INDIRECT,
 * CONSEQUENTIAL, SPECIAL OR OTHER DAMAGES ARISING FROM THE USE OF, OR
 * INABILITY TO USE, THE MATERIAL, INCLUDING, BUT NOT LIMITED TO, ANY DAMAGES
 * FOR LOST PROFITS.
 */

package edu.jhuapl.graphs.jfreechart;

import edu.jhuapl.graphs.DataSeriesInterface;
import edu.jhuapl.graphs.Encoding;
import edu.jhuapl.graphs.GraphException;
import edu.jhuapl.graphs.GraphSource;
import edu.jhuapl.graphs.GraphSourceBean;
import edu.jhuapl.graphs.TimePointInterface;
import edu.jhuapl.graphs.TimeResolution;
import edu.jhuapl.graphs.TimeSeriesInterface;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.axis.DateTickUnitType;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.TickUnits;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.block.BlockContainer;
import org.jfree.chart.block.BorderArrangement;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.title.LegendTitle;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.time.Day;
import org.jfree.data.time.Hour;
import org.jfree.data.time.Month;
import org.jfree.data.time.Quarter;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.Week;
import org.jfree.data.time.Year;
import org.jfree.ui.HorizontalAlignment;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;

import java.awt.*;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * An implementation of {@link GraphSource} for JFreeChart.  Currently, this class produces Time Series graphs.
 *
 * <p>To use this class, you need to use one of the two forms of initialization, either
 * <pre>JFreeChartGraphSource source = new JFreeChartGraphSource(listOfSeries, graphParams);</pre>
 * or
 * <pre>
 * JFreeChartGraphSource source = new JFreeChartGraphSource();
 * source.setData(listOfSeries);
 * source.setParams(graphParams);
 * source.initialize();
 * </pre>
 * Note that the former form will automatically initialize the internal JFreeChart graph.  Note that if you try to
 * {@link #renderGraph(int, int, Encoding) render} the graph without first initializing the internal JFreeChart graph, a
 * {@link DataSourceException} will be thrown. </p> <p>This class is backed by a {@link JFreeChart} instance, which can
 * be accessed by {@link #getChart()}.
 *
 * @see GraphSource
 */
public class JFreeChartTimeSeriesGraphSource extends JFreeChartBaseSource implements GraphSourceBean {

    protected static final TimeResolution DEFAULT_RESOLUTION = TimeResolution.DAILY;
    protected static final Stroke DEFAULT_GRAPH_STROKE = new BasicStroke();
    protected static final String DEFAULT_DOMAIN_LABEL = "Date";
    protected static final String DEFAULT_RANGE_LABEL = "Value";
    //Setting the domain axis lower margin to 0 places the start of the series directly on the y-axis origin - there's no gap.
    private static final Double DEFAULT_DOMAIN_AXIS_LOWER_MARGIN = new Double(.04);
    //Setting the domain axis upper margin to 0 places places the end of the series squarely at the edge of the plot - there's no gap.
    private static final Double DEFAULT_DOMAIN_AXIS_UPPER_MARGIN = new Double(.04);

    /**
     * Specifies the lower margin for the domain axis. Expects a {@link Double}}.
     */
    public static final String DOMAIN_AXIS_LOWER_MARGIN = "DomainAxisLowerMargin";

    /**
     * Specifies the upper margin for the domain axis. Expects a {@link Double}.
     */
    public static final String DOMAIN_AXIS_UPPER_MARGIN = "DomainAxisUpperMargin";

    /**
     * Specifies a particular date axis object to be used in this graph.  Expects a {@link org.jfree.chart.axis.DateAxis}.
     */
    public static final String DATE_AXIS = "DateAxis";

    /**
     * Creates an empty, uninitialized JFreeChartGraphSource.
     */
    public JFreeChartTimeSeriesGraphSource() {
        super();
    }

    /**
     * Creates and initializes a JFreeChartGraphSource with the given data series and graph parameters.
     *
     * @param data        the list of {@link TimeSeriesInterface time series}.
     * @param graphParams the graph parameters
     * @throws GraphException if the initialization fails
     */
    public JFreeChartTimeSeriesGraphSource(List<? extends TimeSeriesInterface> data,
            Map<String, Object> graphParams) throws GraphException {
        this();
        setData(data);
        setParams(graphParams);
        initialize();
    }

    /**
     * Initializes the graph.  This method generates the backing {@link JFreeChart} from the time series and graph
     * parameter data.
     *
     * @throws GraphException if the initialization fails
     */
    public void initialize() throws GraphException {
        String title = getParam(GraphSource.GRAPH_TITLE, String.class, DEFAULT_TITLE);
        String xLabel = getParam(GraphSource.GRAPH_X_LABEL, String.class, DEFAULT_DOMAIN_LABEL);
        String yLabel = getParam(GraphSource.GRAPH_Y_LABEL, String.class, DEFAULT_RANGE_LABEL);
        Shape graphShape = getParam(GraphSource.GRAPH_SHAPE, Shape.class, DEFAULT_GRAPH_SHAPE);
        Paint graphColor = getParam(GraphSource.GRAPH_COLOR, Paint.class, DEFAULT_GRAPH_COLOR);
        boolean legend = getParam(GraphSource.GRAPH_LEGEND, Boolean.class, DEFAULT_GRAPH_LEGEND);
        boolean graphToolTip = getParam(GraphSource.GRAPH_TOOL_TIP, Boolean.class, DEFAULT_GRAPH_TOOL_TIP);
        Stroke graphStroke = getParam(GraphSource.GRAPH_STROKE, Stroke.class, DEFAULT_GRAPH_STROKE);
        Font titleFont = getParam(GraphSource.GRAPH_FONT, Font.class, DEFAULT_GRAPH_TITLE_FONT);
        boolean graphBorder = getParam(GraphSource.GRAPH_BORDER, Boolean.class, DEFAULT_GRAPH_BORDER);
        boolean legendBorder = getParam(GraphSource.LEGEND_BORDER, Boolean.class, DEFAULT_LEGEND_BORDER);
        Double offset = getParam(GraphSource.AXIS_OFFSET, Double.class, DEFAULT_AXIS_OFFSET);

        checkSeriesType(data);
        @SuppressWarnings("unchecked")
        List<? extends TimeSeriesInterface> timeData = (List<? extends TimeSeriesInterface>) data;

        TimeSeriesCollection dataset = new TimeSeriesCollection();
        int seriesCount = 1;
        for (TimeSeriesInterface series : timeData) {
            dataset.addSeries(buildTimeSeries(series, seriesCount));
            seriesCount += 1;
        }

        // actually create the chart
        this.chart = ChartFactory.createTimeSeriesChart(title, xLabel, yLabel, dataset, false, graphToolTip, false);

        // start customizing it
        Paint backgroundColor = getParam(GraphSource.BACKGROUND_COLOR, Paint.class, DEFAULT_BACKGROUND_COLOR);
        Paint plotColor = getParam(JFreeChartTimeSeriesGraphSource.PLOT_COLOR, Paint.class, backgroundColor);
        Paint graphDomainGridlinePaint = getParam(GraphSource.GRAPH_DOMAIN_GRIDLINE_PAINT, Paint.class,
                backgroundColor);
        Paint graphRangeGridlinePaint = getParam(GraphSource.GRAPH_RANGE_GRIDLINE_PAINT, Paint.class,
                backgroundColor);

        this.chart.setBackgroundPaint(backgroundColor);
        XYPlot plot = chart.getXYPlot();
        plot.setBackgroundPaint(plotColor);
        plot.setAxisOffset(new RectangleInsets(offset, offset, offset, offset));
        plot.setDomainGridlinePaint(graphDomainGridlinePaint);
        plot.setRangeGridlinePaint(graphRangeGridlinePaint);

        if (graphBorder) {

        } else {
            plot.setOutlinePaint(null);
        }

        //Use a TextTitle to change the font of the graph title
        TextTitle title1 = new TextTitle();
        title1.setText(title);
        title1.setFont(titleFont);
        chart.setTitle(title1);

        //Makes a wrapper for the legend to remove the border around it
        if (legend) {
            LegendTitle legend1 = new LegendTitle(chart.getPlot());
            BlockContainer wrapper = new BlockContainer(new BorderArrangement());
            if (legendBorder) {
                wrapper.setFrame(new BlockBorder(1, 1, 1, 1));
            } else {
                wrapper.setFrame(new BlockBorder(0, 0, 0, 0));
            }
            BlockContainer items = legend1.getItemContainer();
            items.setPadding(2, 10, 5, 2);
            wrapper.add(items);
            legend1.setWrapper(wrapper);
            legend1.setPosition(RectangleEdge.BOTTOM);
            legend1.setHorizontalAlignment(HorizontalAlignment.CENTER);

            if (params.get(GraphSource.LEGEND_FONT) instanceof Font) {
                legend1.setItemFont(((Font) params.get(GraphSource.LEGEND_FONT)));
            }

            chart.addSubtitle(legend1);
        }

        boolean include0 = getParam(GraphSource.GRAPH_RANGE_INCLUDE_0, Boolean.class, true);
        NumberAxis numAxis = (NumberAxis) plot.getRangeAxis();
        double rangeLower = getParam(GraphSource.GRAPH_RANGE_LOWER_BOUND, Double.class, numAxis.getLowerBound());
        double rangeUpper = getParam(GraphSource.GRAPH_RANGE_UPPER_BOUND, Double.class, numAxis.getUpperBound());
        boolean graphRangeIntegerTick = getParam(GraphSource.GRAPH_RANGE_INTEGER_TICK, Boolean.class, false);
        boolean graphRangeMinorTickVisible = getParam(GraphSource.GRAPH_RANGE_MINOR_TICK_VISIBLE, Boolean.class,
                true);

        if (include0) {
            rangeLower = 0;
        }

        numAxis.setRange(rangeLower, rangeUpper);

        if (graphRangeIntegerTick) {
            numAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
        }

        numAxis.setMinorTickMarksVisible(graphRangeMinorTickVisible);
        setupFont(numAxis, GraphSource.GRAPH_Y_AXIS_FONT);

        if (params.get(GraphSource.GRAPH_Y_AXIS_LABEL_FONT) instanceof Font) {
            numAxis.setLabelFont(((Font) params.get(GraphSource.GRAPH_Y_AXIS_LABEL_FONT)));
        }

        TimeResolution minimumResolution = getMinimumResolution(timeData);
        DateFormat dateFormat = getParam(GraphSource.GRAPH_DATE_FORMATTER, DateFormat.class,
                new DefaultDateFormatFactory().getFormat(minimumResolution));

        if (params.get(DATE_AXIS) instanceof DateAxis) {
            DateAxis dateAxis = (DateAxis) params.get(DATE_AXIS);
            dateAxis.setLabel(xLabel);
            plot.setDomainAxis(dateAxis);
        }
        DateAxis dateAxis = ((DateAxis) plot.getDomainAxis());
        dateAxis.setDateFormatOverride(dateFormat);

        if (params.get(GraphSource.GRAPH_X_AXIS_LABEL_FONT) instanceof Font) {
            dateAxis.setLabelFont(((Font) params.get(GraphSource.GRAPH_X_AXIS_LABEL_FONT)));
        }

        int minTick = getParam(GraphSource.GRAPH_MIN_DOMAIN_TICK, Integer.class, 1);
        if (minTick <= 0) {
            minTick = 1;
        }

        dateAxis.setTickUnit(getDateTickUnit(minimumResolution, minTick), false, false);
        //dateAxis.setMinorTickMarksVisible(true);
        //dateAxis.setMinorTickCount(7);
        dateAxis.setMinorTickMarkOutsideLength(2);

        Integer minorTick = getParam(GraphSource.GRAPH_MINOR_TICKS, Integer.class, null);
        if (minorTick != null) {
            int minorVal = minorTick;
            if (minorVal > 0) {
                dateAxis.setMinorTickCount(minorVal);
            }
        }

        setupFont(dateAxis, GraphSource.GRAPH_X_AXIS_FONT);

        //double lowerMargin = getParam(DOMAIN_AXIS_LOWER_MARGIN, Double.class, DEFAULT_DOMAIN_AXIS_LOWER_MARGIN);
        double lowerMargin = getParam(DOMAIN_AXIS_LOWER_MARGIN, Double.class, DEFAULT_DOMAIN_AXIS_LOWER_MARGIN);
        dateAxis.setLowerMargin(lowerMargin);

        //double upperMargin = getParam(DOMAIN_AXIS_UPPER_MARGIN, Double.class, DEFAULT_DOMAIN_AXIS_UPPER_MARGIN);
        double upperMargin = getParam(DOMAIN_AXIS_UPPER_MARGIN, Double.class, DEFAULT_DOMAIN_AXIS_UPPER_MARGIN);
        dateAxis.setUpperMargin(upperMargin);

        Date domainLower = getParam(GraphSource.GRAPH_DOMAIN_LOWER_BOUND, Date.class, dateAxis.getMinimumDate());
        Date domainUpper = getParam(GraphSource.GRAPH_DOMAIN_UPPER_BOUND, Date.class, dateAxis.getMaximumDate());

        dateAxis.setRange(domainLower, domainUpper);

        // depending on the domain axis range, display either 1 tick per day, week, month or year
        TickUnits standardUnits = new TickUnits();
        standardUnits.add(new DateTickUnit(DateTickUnitType.DAY, 1));
        standardUnits.add(new DateTickUnit(DateTickUnitType.DAY, 7));
        standardUnits.add(new DateTickUnit(DateTickUnitType.MONTH, 1));
        standardUnits.add(new DateTickUnit(DateTickUnitType.YEAR, 1));
        dateAxis.setStandardTickUnits(standardUnits);

        TimeSeriesRenderer renderer = new TimeSeriesRenderer(dataset);
        setupRenderer(renderer, graphColor, graphShape, graphStroke);
        renderer.setBaseFillPaint(Color.BLACK);
        renderer.setSeriesOutlinePaint(0, Color.WHITE);

        //renderer.setUseOutlinePaint(true);

        plot.setRenderer(renderer);
        this.initialized = true;
    }

    protected void setupRenderer(TimeSeriesRenderer renderer, Paint graphColor, Shape graphShape,
            Stroke graphStroke) {
        int count = 0;
        for (DataSeriesInterface si : data) {
            Paint seriesColor = getParam(si.getMetadata(), GraphSource.SERIES_COLOR, Paint.class, graphColor);
            Shape seriesShape = getParam(si.getMetadata(), GraphSource.SERIES_SHAPE, Shape.class, graphShape);
            Stroke seriesStroke = getParam(si.getMetadata(), GraphSource.SERIES_STROKE, Stroke.class, graphStroke);
            boolean seriesVisible = getParam(si.getMetadata(), GraphSource.SERIES_VISIBLE, Boolean.class, true);
            boolean seriesLinesVisible = getParam(si.getMetadata(), GraphSource.SERIES_LINES_VISIBLE, Boolean.class,
                    true);

            renderer.setSeriesPaint(count, seriesColor);
            renderer.setSeriesFillPaint(count, seriesColor);
            renderer.setSeriesOutlinePaint(count, seriesColor);
            renderer.setSeriesShape(count, seriesShape);
            renderer.setSeriesStroke(count, seriesStroke);
            renderer.setSeriesVisible(count, seriesVisible);
            renderer.setSeriesLinesVisible(count, seriesLinesVisible);

            count += 1;
        }

        renderer.setDrawOutlines(false);
    }

    private TimeSeries buildTimeSeries(TimeSeriesInterface series, int i) throws GraphException {
        Map<String, Object> metadata = series.getMetadata();
        TimeResolution resolution;
        if (metadata.get(GraphSource.SERIES_TIME_RESOLUTION) instanceof TimeResolution) {
            resolution = (TimeResolution) metadata.get(GraphSource.SERIES_TIME_RESOLUTION);
        } else {
            resolution = DEFAULT_RESOLUTION;
        }

        String title = null;
        if (metadata.get(GraphSource.SERIES_TITLE) instanceof String) {
            title = (String) metadata.get(GraphSource.SERIES_TITLE);
        } else {
            title = "series" + i;
        }

        // This method will throw a graph exception if multiple points with the same resolved date are
        // present
        checkSeries(series, resolution);

        TimeSeries s = new TimeSeries(title);
        for (TimePointInterface point : series.getPoints()) {
            s.add(new MetadataTimeSeriesDataItem(makePeriod(point, resolution), point.getValue(),
                    point.getMetadata()));
        }

        return s;
    }

    private static void checkSeriesType(List<? extends DataSeriesInterface> data) throws GraphException {
        for (DataSeriesInterface dataSeries : data) {
            if (!(dataSeries instanceof TimeSeriesInterface)) {
                throw new GraphException("All Series in time series graph must be date based");
            }
        }
    }

    private static void checkSeries(TimeSeriesInterface series, TimeResolution resolution) throws GraphException {
        Set<Date> seenDates = new HashSet<Date>();
        for (TimePointInterface point : series.getPoints()) {
            Date kernelDate = getKernelDate(point.getDescriminator(), resolution);
            if (seenDates.contains(kernelDate)) {
                throw new GraphException("Time series already contains point with date " + kernelDate);
            }
            seenDates.add(kernelDate);
        }

    }

    private static Date getKernelDate(Date d, TimeResolution resolution) {
        Calendar c = Calendar.getInstance();
        c.setTime(d);

        switch (resolution) {
        case HOURLY:
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            break;
        case DAILY:
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            break;
        case WEEKLY:
            c.set(Calendar.DAY_OF_WEEK, 1);
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            break;
        case MONTHLY:
            c.set(Calendar.WEEK_OF_MONTH, 1);
            c.set(Calendar.DAY_OF_WEEK, 1);
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            break;
        case QUARTERLY:
            int numMonthsInQuarter = 3;
            // sets to either January 1st, April 1st, July 1st or October 1st
            c.set(Calendar.MONTH, c.get(Calendar.MONTH) / numMonthsInQuarter * numMonthsInQuarter);
            c.set(Calendar.DAY_OF_MONTH, 1);
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            break;
        case YEARLY:
            c.set(Calendar.MONTH, Calendar.JANUARY);
            c.set(Calendar.WEEK_OF_MONTH, 1);
            c.set(Calendar.DAY_OF_WEEK, 1);
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
            c.set(Calendar.SECOND, 0);
            c.set(Calendar.MILLISECOND, 0);
            break;
        }

        return c.getTime();
    }

    private static RegularTimePeriod makePeriod(TimePointInterface point, TimeResolution resolution)
            throws GraphException {
        switch (resolution) {
        case DAILY:
            return makeDay(point);
        case MONTHLY:
            return makeMonth(point);
        case WEEKLY:
            return makeWeek(point);
        case YEARLY:
            return makeYear(point);
        case HOURLY:
            return makeHour(point);
        case QUARTERLY:
            return makeQuarter(point);
        default:
            throw new GraphException("Unexpected resolution \"" + resolution.toString() + "\"");
        }
    }

    private static Day makeDay(TimePointInterface point) {
        return new Day(point.getDescriminator());
    }

    private static Week makeWeek(TimePointInterface point) {
        return new Week(point.getDescriminator());
    }

    private static Month makeMonth(TimePointInterface point) {
        return new Month(point.getDescriminator());
    }

    private static Quarter makeQuarter(TimePointInterface point) {
        return new Quarter(point.getDescriminator());
    }

    private static Year makeYear(TimePointInterface point) {
        return new Year(point.getDescriminator());
    }

    private static Hour makeHour(TimePointInterface point) {
        return new Hour(point.getDescriminator());
    }

    private static DateTickUnit getDateTickUnit(TimeResolution minimumResolution, int qty) throws GraphException {
        DateTickUnitType dateTypeUnit;
        switch (minimumResolution) {
        case HOURLY:
            dateTypeUnit = DateTickUnitType.HOUR;
            break;
        case DAILY:
        case WEEKLY:
            dateTypeUnit = DateTickUnitType.DAY;
            break;
        case MONTHLY:
            dateTypeUnit = DateTickUnitType.MONTH;
            break;
        case YEARLY:
            dateTypeUnit = DateTickUnitType.YEAR;
            break;
        default:
            throw new GraphException("Unrecognized resolution \"" + minimumResolution + "\"");
        }

        return new DateTickUnit(dateTypeUnit, qty);
    }

    private static TimeResolution getMinimumResolution(List<? extends TimeSeriesInterface> data) {
        TimeResolution minimum = null;
        for (TimeSeriesInterface tsi : data) {
            if (tsi.getMetadata().get(GraphSource.SERIES_TIME_RESOLUTION) instanceof TimeResolution) {
                TimeResolution candidate = (TimeResolution) tsi.getMetadata()
                        .get(GraphSource.SERIES_TIME_RESOLUTION);
                if (minimum == null || candidate.ordinal() < minimum.ordinal()) {
                    minimum = candidate;
                }
            }
        }

        return minimum != null ? minimum : TimeResolution.DAILY;
    }
}