ch.agent.crnickl.demo.stox.Chart.java Source code

Java tutorial

Introduction

Here is the source code for ch.agent.crnickl.demo.stox.Chart.java

Source

/*
 *   Copyright 2011-2013 Hauser Olsson GmbH
 *
 * 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
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package ch.agent.crnickl.demo.stox;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import org.apache.batik.svggen.SVGGeneratorContext;
import org.apache.batik.svggen.SVGGraphics2D;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.SegmentedTimeline;
import org.jfree.chart.encoders.ImageEncoder;
import org.jfree.chart.encoders.ImageEncoderFactory;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.TimePeriodAnchor;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;

import ch.agent.core.KeyedException;
import ch.agent.crnickl.demo.stox.DemoConstants.K;
import ch.agent.t2.time.Range;
import ch.agent.t2.time.TimeDomain;
import ch.agent.t2.timeseries.Observation;
import ch.agent.t2.timeseries.TimeAddressable;
import ch.agent.t2.timeutil.JavaDateUtil;

/**
 * Draw charts using JFreeChart.
 * 
 * @author Jean-Paul Vetterli
 */
public class Chart {

    /**
     * A ChartSeries packs a time series and some important chart parameters.
     *
     */
    public static class ChartSeries {
        private TimeAddressable<Double> series;
        private String name;
        private boolean line;
        private int subPlotIndex;
        private int weight;

        /**
         * Construct a ChartSeries.
         * 
         * @param series a time series
         * @param name a short string describing the time series uniquely 
         */
        public ChartSeries(TimeAddressable<Double> series, String name) {
            if (series == null)
                throw new IllegalArgumentException("series null");
            this.series = series;
            this.name = name;
            this.line = true;
            this.weight = 1;
            this.subPlotIndex = -1;
        }

        /**
         * Return the time series.
         * @return a time series
         */
        public TimeAddressable<Double> getTimeSeries() {
            return series;
        }

        /**
         * Return the name
         * @return a string
         */
        public String getName() {
            return name;
        }

        /**
         * Set the line mode.
         * 
         * @param line true for a line chart, false for a bar chart
         */
        public void setLine(boolean line) {
            this.line = line;
        }

        /**
         * Return true for a line chart and false for a bar chart. Default: true.
         * @return true if a line chart
         */
        public boolean isLine() {
            return line;
        }

        /**
         * Return the relative vertical size of the subplot area.
         * Ignored when using an existing subplot.
         * Default: 1.
         * 
         * @return a number
         */
        public int getWeight() {
            return weight;
        }

        /**
         * Set the relative vertical size of the subplot area.
         * 
         * @param weight a number
         */
        public void setWeight(int weight) {
            this.weight = weight;
        }

        /**
         * Return the subplot index. If non positivee, put the series in a new area.
         * Else put it in the subplot indicated. It is a 1-based index. Default value is 0.
         * 
         * @return a number
         */
        public int getSubPlotIndex() {
            return subPlotIndex;
        }

        /**
         * Set the subplot index for this series.
         * 
         * @param subPlotIndex a number
         */
        public void setSubPlotIndex(int subPlotIndex) {
            this.subPlotIndex = subPlotIndex;
        }
    }

    private String title;
    private Range range;
    private boolean withLegend;
    private List<ChartSeries> chartSeries;
    private JFreeChart chart;

    /**
     * Construct a StockChart object. Set up the map for runtime arguments and
     * initialize it with valid names and default values.
     */
    public Chart() {
        this.chartSeries = new ArrayList<Chart.ChartSeries>();
        /* eliminate time zone and DST effects */
        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    }

    /**
     * Set the range of data to display. If null or if not specified, display 
     * full range. 
     * Using this method clears the current chart and the series already added.
     * 
     * @param range a range or null for full range
     */
    public void setRange(Range range) {
        this.range = range;
        this.chart = null;
        this.chartSeries.clear();
    }

    /**
     * Set the chart title.
     * Using this method clears the current chart.
     * 
     * @param title a string
     */
    public void setTitle(String title) {
        this.title = title;
        this.chart = null;
    }

    /**
     * Set legend visibility. By default the legend is not displayed.
     * Using this method clears the current chart.
     * 
     * @param withLegend if true legend will be displayed
     */
    public void setWithLegend(boolean withLegend) {
        this.withLegend = withLegend;
        this.chart = null;
    }

    /**
     * Add a ChartSeries.
     * Using this method clears the current chart.
     * 
     * @param chartSeries a ChartSeries
     */
    public void addChartSeries(ChartSeries chartSeries) {
        this.chartSeries.add(chartSeries);
        this.chart = null;
    }

    /**
     * Save a chart into a file. It is possible to save into multiple
     * files or with various dimensions without recompiling the chart.
     * 
     * @param outputFile a file name
     * @param chartWidth a positive number
     * @param chartHeight a positive number
     * @throws KeyedException
     */
    public void save(String outputFile, int chartWidth, int chartHeight) throws KeyedException {
        saveChart(getChart(), outputFile, chartWidth, chartHeight);
    }

    private JFreeChart getChart() throws KeyedException {
        if (chart == null)
            chart = makeChart();
        return chart;
    }

    private JFreeChart makeChart() throws KeyedException {

        if (chartSeries.size() == 0)
            throw new IllegalStateException("addChartSeries() not called");

        if (range == null) {
            for (ChartSeries s : chartSeries) {
                if (range == null)
                    range = s.getTimeSeries().getRange();
                else
                    range = range.union(s.getTimeSeries().getRange());
            }
        }

        // use number axis for dates, with special formatter
        DateAxis dateAxis = new DateAxis();
        dateAxis.setDateFormatOverride(new CustomDateFormat("M/d/y"));
        if (range.getTimeDomain().getLabel().equals("workweek"))
            dateAxis.setTimeline(SegmentedTimeline.newMondayThroughFridayTimeline());

        // combined plot with shared date axis
        CombinedDomainXYPlot plot = new CombinedDomainXYPlot(dateAxis);

        for (ChartSeries s : chartSeries) {
            makeSubPlot(plot, s);
        }

        // make the chart, remove the legend, set the title
        JFreeChart chart = new JFreeChart(plot);
        if (!withLegend)
            chart.removeLegend();
        chart.setBackgroundPaint(Color.white);
        chart.setTitle(new TextTitle(title));

        return chart;
    }

    private void makeSubPlot(CombinedDomainXYPlot container, ChartSeries series) throws KeyedException {
        int index = series.getSubPlotIndex();
        int nextDatasetOffset = 0;
        XYPlot plot = null;
        try {
            if (index > 0) {
                plot = (XYPlot) container.getSubplots().get(index - 1);
                nextDatasetOffset = plot.getDatasetCount();
            }
        } catch (Exception e) {
            throw K.CHART_SUBPLOT_ERR.exception(e, index);
        }
        if (plot == null)
            plot = series.isLine() ? getLinePlot() : getBarPlot();
        XYItemRenderer renderer = series.isLine() ? getLineRenderer() : getBarRenderer();
        plot.setRenderer(nextDatasetOffset, renderer);
        plot.setDataset(nextDatasetOffset, getDataset(series.getTimeSeries(), series.getName()));
        if (index < 1)
            container.add(plot, series.getWeight());
    }

    private XYItemRenderer getLineRenderer() throws KeyedException {
        XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer();
        lineRenderer.setDrawSeriesLineAsPath(true);
        lineRenderer.setSeriesStroke(0,
                new BasicStroke(getStrokeWidth(), BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));
        lineRenderer.setBaseShapesVisible(false);
        return lineRenderer;
    }

    private float getStrokeWidth() {
        // determine the stroke width from the "density" of the data
        return Math.max(1f, 3f - 0.4f * (range.getSize() / 100));
    }

    private XYItemRenderer getBarRenderer() throws KeyedException {
        return new XYBarRenderer();
    }

    private XYPlot getLinePlot() throws KeyedException {
        // use a number axis on the left side (default)
        NumberAxis axis = new NumberAxis();
        axis.setAutoRangeIncludesZero(false);
        XYPlot plot = new XYPlot(null, null, axis, null);
        return plot;
    }

    private XYPlot getBarPlot() throws KeyedException {
        // use a number axis on the right side with a special formatter for millions
        NumberAxis axis = new NumberAxis();
        axis.setAutoRangeIncludesZero(false);
        axis.setNumberFormatOverride(new NumberFormatForMillions());
        XYPlot plot = new XYPlot(null, null, axis, null);
        plot.setRangeAxisLocation(AxisLocation.TOP_OR_RIGHT);
        return plot;
    }

    /**
     * Save the chart in a file. Currently the image types supported are
     * PNG and SVG. They are selected from the file extension.
     * 
     * @param chart a non-null {@link JFreeChart}
     * @param fileName a non-null file name
     * @param width a positive number
     * @param height a positive number
     * @throws Exception
     */
    private void saveChart(JFreeChart chart, String fileName, int width, int height) throws KeyedException {
        if (width <= 0 || height <= 0)
            throw new IllegalArgumentException("width or height not positive");
        if (fileName.toUpperCase().endsWith(".PNG")) {
            saveChartAsPNG(chart, fileName, width, height);
        } else if (fileName.toUpperCase().endsWith(".SVG"))
            saveChartAsSVG(chart, fileName, width, height);
        else
            throw K.CHART_SUPPORT_ERR.exception(fileName);
    }

    private void saveChartAsPNG(JFreeChart chart, String fileName, int width, int height) throws KeyedException {
        OutputStream out = null;
        try {
            out = new BufferedOutputStream(new FileOutputStream(fileName));
            BufferedImage bufferedImage = chart.createBufferedImage(width, height, null);
            ImageEncoder imageEncoder = ImageEncoderFactory.newInstance("png");
            imageEncoder.encode(bufferedImage, out);
        } catch (Exception e) {
            throw K.JFC_OUTPUT_ERR.exception(e, fileName);
        } finally {
            try {
                if (out != null)
                    out.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void saveChartAsSVG(JFreeChart chart, String fileName, int width, int height) throws KeyedException {
        Writer out = null;
        try {
            out = new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8");
            String svgNS = "http://www.w3.org/2000/svg";
            DOMImplementation di = DOMImplementationRegistry.newInstance().getDOMImplementation("XML 1.0");
            Document document = di.createDocument(svgNS, "svg", null);

            SVGGeneratorContext ctx = SVGGeneratorContext.createDefault(document);
            ctx.setEmbeddedFontsOn(true);
            SVGGraphics2D svgGenerator = new CustomSVGGraphics2D(ctx, true, 100, true);
            svgGenerator.setSVGCanvasSize(new Dimension(width, height));
            chart.draw(svgGenerator, new Rectangle2D.Double(0, 0, width, height));
            boolean useCSS = true;
            svgGenerator.stream(out, useCSS);
            svgGenerator.dispose();
        } catch (Exception e) {
            throw K.JFC_OUTPUT_ERR.exception(e, fileName);
        } finally {
            try {
                if (out != null)
                    out.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private TimeSeries convertToJFCTimeSeries(boolean interpolate, String name, TimeAddressable<Double> ts)
            throws KeyedException {
        Constructor<? extends RegularTimePeriod> constructor = getPeriodConstructor(ts.getTimeDomain());
        TimeSeries timeSeries = new TimeSeries(name);
        for (Observation<Double> obs : ts) {
            Double value = obs.getValue();
            if (ts.isMissing(value)) {
                if (interpolate)
                    continue;
            }
            Date date = JavaDateUtil.toJavaDate(obs.getTime());
            RegularTimePeriod period = null;
            try {
                period = constructor.newInstance(date);
            } catch (Exception e) {
                throw K.JFC_PERIOD_ERR.exception(e, date.toString());
            }
            timeSeries.add(period, value);
        }
        return timeSeries;
    }

    private Constructor<? extends RegularTimePeriod> getPeriodConstructor(TimeDomain timeDomain)
            throws KeyedException {
        Class<? extends RegularTimePeriod> timeClass = findTimeClass(timeDomain);
        try {
            return timeClass.getConstructor(Date.class);
        } catch (Exception e) {
            throw K.JFC_TIMECLASS_ERR.exception(e, Date.class.getSimpleName());
        }
    }

    private Class<? extends RegularTimePeriod> findTimeClass(TimeDomain timeDomain) throws KeyedException {
        Class<? extends RegularTimePeriod> jfcTimeClass = null;
        // determine JFreeChart type of time from the time domain
        switch (timeDomain.getResolution()) {
        case YEAR:
            jfcTimeClass = org.jfree.data.time.Year.class;
            break;
        case MONTH:
            jfcTimeClass = org.jfree.data.time.Month.class;
            break;
        case DAY:
            jfcTimeClass = org.jfree.data.time.Day.class;
            break;
        case HOUR:
            jfcTimeClass = org.jfree.data.time.Hour.class;
            break;
        case MIN:
            jfcTimeClass = org.jfree.data.time.Minute.class;
            break;
        case SEC:
            jfcTimeClass = org.jfree.data.time.Second.class;
            break;
        case MSEC:
            jfcTimeClass = org.jfree.data.time.Millisecond.class;
            break;
        case USEC:
            throw K.JFC_USEC_ERR.exception();
        default:
            throw new RuntimeException("bug: " + timeDomain.getResolution());
        }
        return jfcTimeClass;
    }

    private XYDataset getDataset(TimeAddressable<Double> ts, String name) throws KeyedException {
        return getDataset(name, convertToJFCTimeSeries(true, name, ts));
    }

    private XYDataset getDataset(String key, TimeSeries series) throws KeyedException {
        TimeSeriesCollection dataset = new TimeSeriesCollection();
        dataset.setXPosition(TimePeriodAnchor.START);
        dataset.addSeries(series);
        return dataset;
    }

    /* ======================================================================= */

    /**
     * NumberFormatForMillions formats large numbers so that they take
     * less space in tick labels.
     */
    @SuppressWarnings("serial")
    public class NumberFormatForMillions extends DecimalFormat {
        @Override
        public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
            if (number > 1000000) {
                super.format(number / 1000000, result, fieldPosition);
                result.append("M");
                return result;
            } else
                return super.format(number, result, fieldPosition);
        }

    }

    /* ======================================================================= */

    /**
     * CustomDateFormat avoids repeating identical dates for tick labels.
     */
    @SuppressWarnings("serial")
    public class CustomDateFormat extends SimpleDateFormat {

        private String previous;

        public CustomDateFormat(String pattern) {
            super(pattern);
        }

        @Override
        public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
            StringBuffer result = super.format(date, toAppendTo, pos);
            if (previous == null || !result.toString().equals(previous)) {
                previous = result.toString();
            } else
                result.setLength(0);
            return result;
        }

    }

    /* ======================================================================= */

    /**
     * CustomSVGGraphics2D is a extension of the Apache Batik
     * {@link SVGGraphics2D} driver.
     */
    public class CustomSVGGraphics2D extends SVGGraphics2D {

        private int geomPercentage = 0;
        private boolean preserveAspectRatio;

        protected CustomSVGGraphics2D(SVGGeneratorContext generatorCtx, boolean textAsShapes, int geomPercentage,
                boolean preserveAspectRatio) {
            super(generatorCtx, textAsShapes);
            this.geomPercentage = geomPercentage;
            this.preserveAspectRatio = preserveAspectRatio;
        }

        @Override
        public Element getRoot(Element svgRoot) {
            svgRoot = domTreeManager.getRoot(svgRoot);
            if (svgCanvasSize != null) {
                // make the image scalable in various viewers
                // and preserve the aspect ratio
                if (geomPercentage > 0) {
                    svgRoot.setAttributeNS(null, SVG_WIDTH_ATTRIBUTE, geomPercentage + "%");
                    svgRoot.setAttributeNS(null, SVG_HEIGHT_ATTRIBUTE, geomPercentage + "%");
                } else {
                    svgRoot.setAttributeNS(null, SVG_WIDTH_ATTRIBUTE, svgCanvasSize.width + "");
                    svgRoot.setAttributeNS(null, SVG_HEIGHT_ATTRIBUTE, svgCanvasSize.height + "");
                }
                svgRoot.setAttributeNS(null, SVG_VIEW_BOX_ATTRIBUTE,
                        "0 0 " + svgCanvasSize.width + " " + svgCanvasSize.height);
                svgRoot.setAttributeNS(null, SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE,
                        preserveAspectRatio ? SVG_MEET_VALUE : SVG_NONE_VALUE);
            }
            return svgRoot;
        }
    }

}