view artifacts/src/main/java/org/dive4elements/river/jfree/StableXYDifferenceRenderer.java @ 9602:6b2496d71936

Reimplemented baseline for tkh. Extended area-dataset to be able to draw baseline.
author gernotbelger
date Tue, 12 Feb 2019 14:08:16 +0100
parents eec4df8165a1
children
line wrap: on
line source
/* Copyright (C) 2011, 2012, 2013 by Bundesanstalt für Gewässerkunde
 * Software engineering by Intevation GmbH
 *
 * This file is Free Software under the GNU AGPL (>=v3)
 * and comes with ABSOLUTELY NO WARRANTY! Check out the
 * documentation coming with Dive4Elements River for details.
 */

/* ===========================================================
 * JFreeChart : a free chart library for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
 *
 * Project Info:  http://www.jfree.org/jfreechart/index.html
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
 * in the United States and other countries.]
 *
 * -------------------------
 * StableXYDifferenceRenderer.java
 * -------------------------
 * (C) Copyright 2003-2008, by Object Refinery Limited and Contributors.
 *
 * Original Author:  David Gilbert (for Object Refinery Limited);
 * Contributor(s):   Richard West, Advanced Micro Devices, Inc. (major rewrite
 *                   of difference drawing algorithm);
 *
 * Changes:
 * --------
 * 30-Apr-2003 : Version 1 (DG);
 * 30-Jul-2003 : Modified entity constructor (CZ);
 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
 * 09-Feb-2004 : Updated to support horizontal plot orientation (DG);
 * 10-Feb-2004 : Added default constructor, setter methods and updated
 *               Javadocs (DG);
 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
 * 30-Mar-2004 : Fixed bug in getNegativePaint() method (DG);
 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
 *               getYValue() (DG);
 * 25-Aug-2004 : Fixed a bug preventing the use of crosshairs (DG);
 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
 * 19-Jan-2005 : Now accesses only primitive values from dataset (DG);
 * 22-Feb-2005 : Override getLegendItem(int, int) to return "line" items (DG);
 * 13-Apr-2005 : Fixed shape positioning bug (id = 1182062) (DG);
 * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG);
 * 04-May-2005 : Override equals() method, renamed get/setPlotShapes() -->
 *               get/setShapesVisible (DG);
 * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG);
 * 16-Jun-2005 : Fix bug (1221021) affecting stroke used for each series (DG);
 * ------------- JFREECHART 1.0.x ---------------------------------------------
 * 24-Jan-2007 : Added flag to allow rounding of x-coordinates, and fixed
 *               bug in clone() (DG);
 * 05-Feb-2007 : Added an extra call to updateCrosshairValues() in
 *               drawItemPass1(), to fix bug 1564967 (DG);
 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
 * 08-Mar-2007 : Fixed entity generation (DG);
 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
 * 23-Apr-2007 : Rewrite of difference drawing algorithm to allow use of
 *               series with disjoint x-values (RW);
 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG);
 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG);
 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG);
 * 05-Nov-2007 : Draw item labels if visible (RW);
 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG);
 */
/*
 * For further changes within the FLYS project, refer to the ChangeLog.
 */
package org.dive4elements.river.jfree;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.apache.log4j.Logger;
import org.dive4elements.river.artifacts.math.Linear;
import org.jfree.chart.LegendItem;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.entity.XYItemEntity;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.labels.XYToolTipGenerator;
import org.jfree.chart.plot.CrosshairState;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.AbstractXYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRendererState;
import org.jfree.chart.urls.XYURLGenerator;
import org.jfree.data.Range;
import org.jfree.data.xy.DefaultXYDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.io.SerialUtilities;
import org.jfree.ui.RectangleEdge;
import org.jfree.util.PaintUtilities;
import org.jfree.util.PublicCloneable;
import org.jfree.util.ShapeUtilities;

import gnu.trove.TDoubleArrayList;

/**
 * A renderer for an {@link XYPlot} that highlights the differences between two
 * series. The example shown here is generated by the
 * <code>DifferenceChartDemo1.java</code> program included in the JFreeChart
 * demo collection:
 * <br>
 * <br>
 * <img src="../../../../../images/StableXYDifferenceRendererSample.png"
 * alt="StableXYDifferenceRendererSample.png" />
 */
public class StableXYDifferenceRenderer extends AbstractXYItemRenderer implements PublicCloneable {

    private static Logger log = Logger.getLogger(StableXYDifferenceRenderer.class);

    public static final int CALCULATE_POSITIVE_AREA = 1;
    public static final int CALCULATE_NEGATIVE_AREA = 2;
    public static final int CALCULATE_ALL_AREA = CALCULATE_POSITIVE_AREA | CALCULATE_NEGATIVE_AREA;

    /** For serialization. */
    private static final long serialVersionUID = -8447915602375584857L;

    private boolean drawArea = true;

    /** The paint used to highlight positive differences (y(0) &gt; y(1)). */
    private transient Paint positivePaint;

    /** The paint used to highlight negative differences (y(0) < y(1)). */
    private transient Paint negativePaint;

    /** Display shapes at each point? */
    private boolean shapesVisible;

    /** Display shapes at each point? */
    private boolean drawOutline;

    /** Which stroke to draw outline with? */
    private Stroke outlineStroke;

    /** Which paint to draw outline with? */
    private Paint outlinePaint;

    /** The shape to display in the legend item. */
    private transient Shape legendShape;

    private final boolean drawOriginalSeries;

    /** NumberFormat to use for area. */
    private NumberFormat areaLabelNumberFormat;

    private int areaCalculationMode;

    private double positiveArea;

    private double negativeArea;

    /** The color of the label showing the calculated area. */
    private Color labelColor;

    /** The background color of the label showing the calculated area. */
    private Color labelBGColor;

    /** Font to draw label of calculated area with. */
    private Font labelFont;

    private Color baseLineColor = null;

    /** Whether or not to draw a label that shows the title of the theme. */
    private boolean drawTitleLabel = false;

    /** Whether or not to draw a label that shows the area of the polygon. */
    private boolean drawAreaLabel = false;

    /** Template to create i18ned label for area. */
    private String areaLabelTamplate;

    /** Arithmetic centroid of drawn polygons. */
    private Point2D.Double centroid;

    /** Number of points that contributed to the centroid. */
    private int centroidNPoints = 0;

    /**
     * This flag controls whether or not the x-coordinates (in Java2D space)
     * are rounded to integers. When set to true, this can avoid the vertical
     * striping that anti-aliasing can generate. However, the rounding may not
     * be appropriate for output in high resolution formats (for example,
     * vector graphics formats such as SVG and PDF).
     *
     * @since 1.0.4
     */
    private final boolean roundXCoordinates;

    /** Holds the minimal x value in screen coordinates, will updated during the draw operation */
    private transient double minimumScreenX = Double.POSITIVE_INFINITY;

    /** Holds the y value in screen coordinates at the minimum x value **/
    private transient double minimumScreenX_Y = Double.POSITIVE_INFINITY;

    /**
     * Creates a new renderer with default attributes.
     */
    public StableXYDifferenceRenderer() {
        this(Color.green, Color.red, false, CALCULATE_ALL_AREA);
    }

    /**
     * Creates a new renderer.
     *
     * @param positivePaint
     *            the highlight color for positive differences
     *            (<code>null</code> not permitted).
     * @param negativePaint
     *            the highlight color for negative differences
     *            (<code>null</code> not permitted).
     * @param shapes
     *            draw shapes?
     */
    private StableXYDifferenceRenderer(final Paint positivePaint, final Paint negativePaint, final boolean shapes, final int areaCalculationMode) {
        if (positivePaint == null) {
            throw new IllegalArgumentException("Null 'positivePaint' argument.");
        }
        if (negativePaint == null) {
            throw new IllegalArgumentException("Null 'negativePaint' argument.");
        }
        this.positivePaint = positivePaint;
        this.negativePaint = negativePaint;
        this.shapesVisible = shapes;
        this.legendShape = new Rectangle2D.Double(-3.0, -3.0, 10.0, 10.0);
        this.roundXCoordinates = false;
        this.drawOutline = true;
        this.outlineStroke = new BasicStroke(1);
        this.outlinePaint = Color.black;
        this.drawOriginalSeries = false;
        this.areaCalculationMode = areaCalculationMode;
        this.labelBGColor = null;
        this.centroid = new Point2D.Double(0, 0);
    }

    public void setAreaCalculationMode(final int areaCalculationMode) {
        this.areaCalculationMode = areaCalculationMode;
    }

    /** Set template to use to create area label (e.g. 'Area=%dm2'). */
    public void setAreaLabelTemplate(final String areaTemplate) {
        this.areaLabelTamplate = areaTemplate;
    }

    public void setAreaLabelNumberFormat(final NumberFormat nf) {
        this.areaLabelNumberFormat = nf;
    }

    public void setShowAreaLabel(final boolean doDrawAreaLabel) {
        this.drawAreaLabel = doDrawAreaLabel;
    }

    public void setShowTitleLabel(final boolean doDrawTitleLabel) {
        this.drawTitleLabel = doDrawTitleLabel;
    }

    /** Set font to paint label with. */
    public void setLabelFont(final Font font) {
        this.labelFont = font;
    }

    /** Set color with which to paint label. */
    public void setLabelColor(final Color color) {
        this.labelColor = color;
    }

    /** Set color with which to paint label bg. */
    public void setLabelBGColor(final Color color) {
        this.labelBGColor = color;
    }

    public void setBaseLineColor(final Color baseLineColor) {
        this.baseLineColor = baseLineColor;
    }

    /**
     * Sets color that is used if drawOutline is true.
     */
    @Override
    public void setOutlinePaint(final Paint outlinePaint) {
        this.outlinePaint = outlinePaint;
    }

    /**
     * Sets Stroke that is used if drawOutline is true.
     */
    @Override
    public void setOutlineStroke(final Stroke stroke) {
        this.outlineStroke = stroke;
    }

    /**
     * Whether or not to draw the 'Shape' of the area (in contrast to
     * shapes at data items).
     */
    public void setDrawOutline(final boolean doDrawOutline) {
        this.drawOutline = doDrawOutline;
    }

    public void setDrawArea(final boolean doDrawArea) {
        this.drawArea = doDrawArea;
    }

    /**
     * Returns the paint used to highlight positive differences.
     *
     * @return The paint (never <code>null</code>).
     *
     * @see #setPositivePaint(Paint)
     */
    private Paint getPositivePaint() {
        return this.positivePaint;
    }

    /**
     * Sets the paint used to highlight positive differences and sends a
     * {@link RendererChangeEvent} to all registered listeners.
     *
     * @param paint
     *            the paint (<code>null</code> not permitted).
     *
     * @see #getPositivePaint()
     */
    public void setPositivePaint(final Paint paint) {
        this.positivePaint = paint;
        fireChangeEvent();
    }

    /**
     * Returns the paint used to highlight negative differences.
     *
     * @return The paint (never <code>null</code>).
     *
     * @see #setNegativePaint(Paint)
     */
    private Paint getNegativePaint() {
        return this.negativePaint;
    }

    /**
     * Sets the paint used to highlight negative differences.
     *
     * @param paint
     *            the paint (<code>null</code> not permitted).
     *
     * @see #getNegativePaint()
     */
    public void setNegativePaint(final Paint paint) {
        this.negativePaint = paint;
        notifyListeners(new RendererChangeEvent(this));
    }

    /**
     * Returns a flag that controls whether or not shapes are drawn for each
     * data value.
     *
     * @return A boolean.
     *
     * @see #setShapesVisible(boolean)
     */
    private boolean getShapesVisible() {
        return this.shapesVisible;
    }

    /**
     * Sets a flag that controls whether or not shapes are drawn for each
     * data value, and sends a {@link RendererChangeEvent} to all registered
     * listeners.
     *
     * @param flag
     *            the flag.
     *
     * @see #getShapesVisible()
     */
    public void setShapesVisible(final boolean flag) {
        this.shapesVisible = flag;
        fireChangeEvent();
    }

    /**
     * Returns the shape used to represent a line in the legend.
     *
     * @return The legend line (never <code>null</code>).
     *
     * @see #setLegendLine(Shape)
     */
    private Shape getLegendLine() {
        return this.legendShape;
    }

    /**
     * Initializes the renderer and returns a state object that should be
     * passed to subsequent calls to the drawItem() method. This method will
     * be called before the first item is rendered, giving the renderer an
     * opportunity to initialize any state information it wants to maintain.
     * The renderer can do nothing if it chooses.
     *
     * @param g2
     *            the graphics device.
     * @param dataArea
     *            the (visible) area inside the axes.
     * @param plot
     *            the plot.
     * @param data
     *            the data.
     * @param info
     *            an optional info collection object to return data back to
     *            the caller.
     *
     * @return A state object.
     */
    @Override
    public XYItemRendererState initialise(final Graphics2D g2, final Rectangle2D dataArea, final XYPlot plot, final XYDataset data,
            final PlotRenderingInfo info) {

        final XYItemRendererState state = super.initialise(g2, dataArea, plot, data, info);
        state.setProcessVisibleItemsOnly(false);
        return state;
    }

    /**
     * Returns <code>2</code>, the number of passes required by the renderer.
     * The {@link XYPlot} will run through the dataset this number of times.
     *
     * @return The number of passes required by the renderer.
     */
    @Override
    public int getPassCount() {
        return 2;
    }

    /**
     * Adds x/y data to series.
     */
    private static final void addSeries(final DefaultXYDataset ds, final Comparable key, final TDoubleArrayList xs, final TDoubleArrayList ys) {
        ds.addSeries(key, new double[][] { xs.toNativeArray(), ys.toNativeArray() });
    }

    private static List<XYDataset> splitByNaNsOneSeries(final XYDataset dataset) {
        final List<XYDataset> datasets = new ArrayList<>();

        final int N = dataset.getItemCount(0);
        final TDoubleArrayList xs = new TDoubleArrayList(N);
        final TDoubleArrayList ys = new TDoubleArrayList(N);
        for (int i = 0; i < N; ++i) {
            final double x = dataset.getXValue(0, i);
            final double y = dataset.getYValue(0, i);
            if (Double.isNaN(x) || Double.isNaN(y)) {
                if (!xs.isEmpty()) {
                    final DefaultXYDataset ds = new DefaultXYDataset();
                    addSeries(ds, dataset.getSeriesKey(0), xs, ys);
                    datasets.add(ds);
                    xs.resetQuick();
                    ys.resetQuick();
                }
            } else {
                xs.add(x);
                ys.add(y);
            }
        }
        if (!xs.isEmpty()) {
            final DefaultXYDataset ds = new DefaultXYDataset();
            addSeries(ds, dataset.getSeriesKey(0), xs, ys);
            datasets.add(ds);
        }

        return datasets;
    }

    private static final boolean add(final TDoubleArrayList xs, final double x) {
        final int N = xs.size();
        if (N == 0 || xs.getQuick(N - 1) < x) {
            xs.add(x);
            return true;
        }
        return false;
    }

    private static boolean hasNaN(final XYDataset dataset) {
        final int N = dataset.getItemCount(0);
        final int M = dataset.getItemCount(1);

        for (int i = 0; i < N; ++i) {
            final double x = dataset.getXValue(0, i);
            final double y = dataset.getYValue(0, i);
            if (Double.isNaN(x) || Double.isNaN(y)) {
                return true;
            }
        }

        for (int j = 0; j < M; ++j) {
            final double x = dataset.getXValue(1, j);
            final double y = dataset.getYValue(1, j);
            if (Double.isNaN(x) || Double.isNaN(y)) {
                return true;
            }
        }
        return false;
    }

    private static List<XYDataset> splitByNaNsTwoSeries(final XYDataset dataset) {
        final boolean debug = log.isDebugEnabled();

        final List<XYDataset> datasets = new ArrayList<>();

        if (!hasNaN(dataset)) {
            datasets.add(dataset);
            return datasets;
        }

        final int N = dataset.getItemCount(0);
        final int M = dataset.getItemCount(1);

        int i = 0, j = 0;
        // ignore leading NaNs
        for (; i < N; ++i) {
            final double x = dataset.getXValue(0, i);
            final double y = dataset.getYValue(0, i);
            if (!Double.isNaN(x) && !Double.isNaN(y)) {
                break;
            }
        }

        for (; j < M; ++j) {
            final double x = dataset.getXValue(1, j);
            final double y = dataset.getYValue(1, j);
            if (!Double.isNaN(x) && !Double.isNaN(y)) {
                break;
            }
        }

        final TDoubleArrayList six = new TDoubleArrayList();
        final TDoubleArrayList siy = new TDoubleArrayList();
        final TDoubleArrayList sjx = new TDoubleArrayList();
        final TDoubleArrayList sjy = new TDoubleArrayList();

        while (i < N && j < M) {
            int ni = i + 1;
            for (; ni < N && !Double.isNaN(dataset.getXValue(0, ni)); ++ni)
                ;
            for (; ni < N && Double.isNaN(dataset.getXValue(0, ni)); ++ni)
                ;

            int nj = j + 1;
            for (; nj < M && !Double.isNaN(dataset.getXValue(1, nj)); ++nj)
                ;
            for (; nj < M && Double.isNaN(dataset.getXValue(1, nj)); ++nj)
                ;

            if (ni == N && nj == M) { // no more splits
                log.debug("no more splits ....");
                for (; i < ni; ++i) {
                    final double x = dataset.getXValue(0, i);
                    final double y = dataset.getYValue(0, i);
                    if (!Double.isNaN(x) && !Double.isNaN(y) && add(six, x)) {
                        siy.add(y);
                    }
                }
                for (; j < nj; ++j) {
                    final double x = dataset.getXValue(1, j);
                    final double y = dataset.getYValue(1, j);
                    if (!Double.isNaN(x) && !Double.isNaN(y) && add(sjx, x)) {
                        sjy.add(y);
                    }
                }
                if (!six.isEmpty() && !sjx.isEmpty()) {
                    final DefaultXYDataset ds = new DefaultXYDataset();
                    addSeries(ds, dataset.getSeriesKey(0), six, siy);
                    addSeries(ds, dataset.getSeriesKey(1), sjx, sjy);
                    datasets.add(ds);
                }
                break;
            }

            if (debug) {
                log.debug("ni: " + ni + " " + N);
                log.debug("nj: " + nj + " " + M);
            }

            final double xni = ni < N ? dataset.getXValue(0, ni) : Double.MAX_VALUE;

            final double xnj = nj < M ? dataset.getXValue(1, nj) : Double.MAX_VALUE;

            final double xns = Math.min(xni, xnj);

            double pushxi = Double.NaN;
            double pushyi = Double.NaN;
            double pushxj = Double.NaN;
            double pushyj = Double.NaN;

            for (; i < ni; ++i) {
                final double x = dataset.getXValue(0, i);
                final double y = dataset.getYValue(0, i);
                if (Double.isNaN(x) || Double.isNaN(y)) {
                    continue;
                }
                if (x < xns) {
                    if (add(six, x)) {
                        siy.add(y);
                    }
                    continue;
                }
                if (x == xns) { // exact match
                    if (add(six, x)) {
                        siy.add(y);
                    }
                    pushxi = x;
                    pushyi = y;
                } else { // x > xns: intersection
                    if (debug) {
                        log.debug("xns: " + xns);
                        log.debug("x/y: " + x + " / " + y);
                    }
                    final int SIX = six.size();
                    if (SIX > 0) { // should always be true
                        final double yns = Linear.linear(xns, six.getQuick(SIX - 1), x, siy.getQuick(SIX - 1), y);
                        if (debug) {
                            log.debug("intersection at: " + yns);
                        }
                        if (add(six, xns)) {
                            siy.add(yns);
                        }
                        pushxi = xns;
                        pushyi = yns;
                    }
                }
                break; // Split point reached.
            }

            for (; j < nj; ++j) {
                final double x = dataset.getXValue(1, j);
                final double y = dataset.getYValue(1, j);
                if (Double.isNaN(x) || Double.isNaN(y)) {
                    continue;
                }
                if (x < xns) {
                    if (add(sjx, x)) {
                        sjy.add(y);
                    }
                    continue;
                }
                if (x == xns) { // exact match
                    if (add(sjx, x)) {
                        sjy.add(y);
                    }
                    pushxj = x;
                    pushyj = y;
                } else { // x > xns: intersection
                    final int SJX = sjx.size();
                    if (SJX > 0) { // should always be true
                        final double yns = Linear.linear(xns, sjx.getQuick(SJX - 1), x, sjy.getQuick(SJX - 1), y);
                        if (debug) {
                            log.debug("intersection at: " + yns);
                        }
                        if (add(sjx, xns)) {
                            sjy.add(yns);
                        }
                        pushxj = xns;
                        pushyj = yns;
                    }
                }
                break; // Split point reached.
            }

            if (!six.isEmpty() && !sjx.isEmpty()) {
                final DefaultXYDataset ds = new DefaultXYDataset();
                addSeries(ds, dataset.getSeriesKey(0), six, siy);
                addSeries(ds, dataset.getSeriesKey(1), sjx, sjy);
                datasets.add(ds);
            }

            six.resetQuick();
            siy.resetQuick();
            sjx.resetQuick();
            sjy.resetQuick();

            // Push split points.
            if (!Double.isNaN(pushxi)) {
                six.add(pushxi);
                siy.add(pushyi);
            }

            if (!Double.isNaN(pushxj)) {
                sjx.add(pushxj);
                sjy.add(pushyj);
            }
        }

        // Copy the rest.
        for (; i < N; ++i) {
            final double x = dataset.getXValue(0, i);
            final double y = dataset.getXValue(0, i);
            if (!Double.isNaN(x) && !Double.isNaN(y) && add(six, x)) {
                siy.add(y);
            }
        }

        for (; j < M; ++j) {
            final double x = dataset.getXValue(1, j);
            final double y = dataset.getXValue(1, j);
            if (!Double.isNaN(x) && !Double.isNaN(y) && add(sjx, x)) {
                sjy.add(y);
            }
        }

        // Build final dataset.
        if (!six.isEmpty() && !sjx.isEmpty()) {
            final DefaultXYDataset ds = new DefaultXYDataset();
            addSeries(ds, dataset.getSeriesKey(0), six, siy);
            addSeries(ds, dataset.getSeriesKey(1), sjx, sjy);
            datasets.add(ds);
        }

        if (debug) {
            log.debug("datasets after split: " + datasets.size());
        }

        /*
         * log.debug("Begin dump datasets: ");
         * log.debug("Original");
         * i=0;
         * dumpDataset(dataset, "Original" );
         * for (XYDataset set: datasets) {
         * dumpDataset(dataset, "New: " + i++);
         * }
         * log.debug("End dump datasets: ");
         */

        return datasets;
    }

    /*
     * public static void dumpDataset(XYDataset dataset, String msg) {
     * log.debug("Begin dump Dataset: " + msg);
     * int N = dataset.getItemCount(0);
     * int M = dataset.getItemCount(1);
     * int i = 0, j = 0;
     * for (; i < N; ++i) {
     * double x = dataset.getXValue(0, i);
     * double y = dataset.getYValue(0, i);
     * log.debug("0 " + i + " X: "+ x + "Y: "+ y);
     * }
     * for (; j < M; ++j) {
     * double x = dataset.getXValue(1, j);
     * double y = dataset.getYValue(1, j);
     * log.debug("1 " + i + " X: "+ x + "Y: "+ y);
     * }
     * log.debug("End dump Dataset: " + msg);
     * }
     */

    private static List<XYDataset> splitByNaNs(final XYDataset dataset) {

        switch (dataset.getSeriesCount()) {
        case 0:
            return Collections.<XYDataset>emptyList();
        case 1:
            return splitByNaNsOneSeries(dataset);
        default: // two or more
            return splitByNaNsTwoSeries(dataset);
        }
    }

    /**
     * Draws the visual representation of a single data item.
     *
     * @param g2
     *            the graphics device.
     * @param state
     *            the renderer state.
     * @param dataArea
     *            the area within which the data is being drawn.
     * @param info
     *            collects information about the drawing.
     * @param plot
     *            the plot (can be used to obtain standard color
     *            information etc).
     * @param domainAxis
     *            the domain (horizontal) axis.
     * @param rangeAxis
     *            the range (vertical) axis.
     * @param dataset
     *            the dataset.
     * @param series
     *            the series index (zero-based).
     * @param item
     *            the item index (zero-based).
     * @param crosshairState
     *            crosshair information for the plot
     *            (<code>null</code> permitted).
     * @param pass
     *            the pass index.
     */
    @Override
    public void drawItem(final Graphics2D g2, final XYItemRendererState state, final Rectangle2D dataArea, final PlotRenderingInfo info, final XYPlot plot,
            final ValueAxis domainAxis, final ValueAxis rangeAxis, final XYDataset dataset, final int series, final int item,
            final CrosshairState crosshairState, final int pass) {

        switch (pass) {
        case 0:
            for (final XYDataset ds : splitByNaNs(dataset)) {
                drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, ds, series, item, crosshairState);
            }
            break;
        case 1:
            drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState);
            break;
        }
    }

    /**
     * Draws the visual representation of a single data item, first pass.
     *
     * @param x_graphics
     *            the graphics device.
     * @param x_dataArea
     *            the area within which the data is being drawn.
     * @param x_info
     *            collects information about the drawing.
     * @param x_plot
     *            the plot (can be used to obtain standard color
     *            information etc).
     * @param x_domainAxis
     *            the domain (horizontal) axis.
     * @param x_rangeAxis
     *            the range (vertical) axis.
     * @param x_dataset
     *            the dataset.
     * @param x_series
     *            the series index (zero-based).
     * @param x_item
     *            the item index (zero-based).
     * @param x_crosshairState
     *            crosshair information for the plot
     *            (<code>null</code> permitted).
     */
    private void drawItemPass0(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot,
            final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item,
            final CrosshairState x_crosshairState) {

        if (x_series != 0 || x_item != 0)
            return;

        final boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount());

        // check if either series is a degenerate case (i.e. less than 2 points)
        if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend))
            return;

        // check if series are disjoint (i.e. domain-spans do not overlap)
        if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset))
            return;

        // polygon definitions
        final List<Double> l_minuendXs = new LinkedList<>();
        final List<Double> l_minuendYs = new LinkedList<>();
        final List<Double> l_subtrahendXs = new LinkedList<>();
        final List<Double> l_subtrahendYs = new LinkedList<>();
        final List<Double> l_polygonXs = new LinkedList<>();
        final List<Double> l_polygonYs = new LinkedList<>();

        // state
        int l_minuendItem = 0;
        final int l_minuendItemCount = x_dataset.getItemCount(0);
        Double l_minuendCurX = null;
        Double l_minuendNextX = null;
        Double l_minuendCurY = null;
        Double l_minuendNextY = null;
        double l_minuendMaxY = Double.NEGATIVE_INFINITY;
        double l_minuendMinY = Double.POSITIVE_INFINITY;

        int l_subtrahendItem = 0;
        int l_subtrahendItemCount = 0; // actual value set below
        Double l_subtrahendCurX = null;
        Double l_subtrahendNextX = null;
        Double l_subtrahendCurY = null;
        Double l_subtrahendNextY = null;
        double l_subtrahendMaxY = Double.NEGATIVE_INFINITY;
        double l_subtrahendMinY = Double.POSITIVE_INFINITY;

        // if a subtrahend is not specified, assume it is zero
        if (b_impliedZeroSubtrahend) {
            l_subtrahendItem = 0;
            l_subtrahendItemCount = 2;
            l_subtrahendCurX = new Double(x_dataset.getXValue(0, 0));
            l_subtrahendNextX = new Double(x_dataset.getXValue(0, (l_minuendItemCount - 1)));
            l_subtrahendCurY = new Double(0.0);
            l_subtrahendNextY = new Double(0.0);
            l_subtrahendMaxY = 0.0;
            l_subtrahendMinY = 0.0;

            l_subtrahendXs.add(l_subtrahendCurX);
            l_subtrahendYs.add(l_subtrahendCurY);
        } else {
            l_subtrahendItemCount = x_dataset.getItemCount(1);
        }

        boolean b_minuendDone = false;
        boolean b_minuendAdvanced = true;
        boolean b_minuendAtIntersect = false;
        boolean b_minuendFastForward = false;
        boolean b_subtrahendDone = false;
        boolean b_subtrahendAdvanced = true;
        boolean b_subtrahendAtIntersect = false;
        boolean b_subtrahendFastForward = false;
        boolean b_colinear = false;

        boolean b_positive;

        // coordinate pairs
        double l_x1 = 0.0, l_y1 = 0.0; // current minuend point
        double l_x2 = 0.0, l_y2 = 0.0; // next minuend point
        double l_x3 = 0.0, l_y3 = 0.0; // current subtrahend point
        double l_x4 = 0.0, l_y4 = 0.0; // next subtrahend point

        // fast-forward through leading tails
        boolean b_fastForwardDone = false;
        while (!b_fastForwardDone) {
            // get the x and y coordinates
            l_x1 = x_dataset.getXValue(0, l_minuendItem);
            l_y1 = x_dataset.getYValue(0, l_minuendItem);
            l_x2 = x_dataset.getXValue(0, l_minuendItem + 1);
            l_y2 = x_dataset.getYValue(0, l_minuendItem + 1);

            l_minuendCurX = new Double(l_x1);
            l_minuendCurY = new Double(l_y1);
            l_minuendNextX = new Double(l_x2);
            l_minuendNextY = new Double(l_y2);

            if (b_impliedZeroSubtrahend) {
                l_x3 = l_subtrahendCurX.doubleValue();
                l_y3 = l_subtrahendCurY.doubleValue();
                l_x4 = l_subtrahendNextX.doubleValue();
                l_y4 = l_subtrahendNextY.doubleValue();
            } else {
                l_x3 = x_dataset.getXValue(1, l_subtrahendItem);
                l_y3 = x_dataset.getYValue(1, l_subtrahendItem);
                l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1);
                l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1);

                l_subtrahendCurX = new Double(l_x3);
                l_subtrahendCurY = new Double(l_y3);
                l_subtrahendNextX = new Double(l_x4);
                l_subtrahendNextY = new Double(l_y4);
            }

            if (l_x2 <= l_x3) {
                // minuend needs to be fast forwarded
                l_minuendItem++;
                b_minuendFastForward = true;
                continue;
            }

            if (l_x4 <= l_x1) {
                // subtrahend needs to be fast forwarded
                l_subtrahendItem++;
                b_subtrahendFastForward = true;
                continue;
            }

            // check if initial polygon needs to be clipped
            if ((l_x3 < l_x1) && (l_x1 < l_x4)) {
                // project onto subtrahend
                final double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3);
                l_subtrahendCurX = l_minuendCurX;
                l_subtrahendCurY = new Double((l_slope * l_x1) + (l_y3 - (l_slope * l_x3)));

                l_subtrahendXs.add(l_subtrahendCurX);
                l_subtrahendYs.add(l_subtrahendCurY);
            }

            if ((l_x1 < l_x3) && (l_x3 < l_x2)) {
                // project onto minuend
                final double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1);
                l_minuendCurX = l_subtrahendCurX;
                l_minuendCurY = new Double((l_slope * l_x3) + (l_y1 - (l_slope * l_x1)));

                l_minuendXs.add(l_minuendCurX);
                l_minuendYs.add(l_minuendCurY);
            }

            l_minuendMaxY = l_minuendCurY.doubleValue();
            l_minuendMinY = l_minuendCurY.doubleValue();
            l_subtrahendMaxY = l_subtrahendCurY.doubleValue();
            l_subtrahendMinY = l_subtrahendCurY.doubleValue();

            b_fastForwardDone = true;
        }

        // start of algorithm
        while (!b_minuendDone && !b_subtrahendDone) {
            if (!b_minuendDone && !b_minuendFastForward && b_minuendAdvanced) {
                l_x1 = x_dataset.getXValue(0, l_minuendItem);
                l_y1 = x_dataset.getYValue(0, l_minuendItem);
                l_minuendCurX = new Double(l_x1);
                l_minuendCurY = new Double(l_y1);

                if (!b_minuendAtIntersect) {
                    l_minuendXs.add(l_minuendCurX);
                    l_minuendYs.add(l_minuendCurY);
                }

                l_minuendMaxY = Math.max(l_minuendMaxY, l_y1);
                l_minuendMinY = Math.min(l_minuendMinY, l_y1);

                l_x2 = x_dataset.getXValue(0, l_minuendItem + 1);
                l_y2 = x_dataset.getYValue(0, l_minuendItem + 1);
                l_minuendNextX = new Double(l_x2);
                l_minuendNextY = new Double(l_y2);
            }

            // never updated the subtrahend if it is implied to be zero
            if (!b_impliedZeroSubtrahend && !b_subtrahendDone && !b_subtrahendFastForward && b_subtrahendAdvanced) {
                l_x3 = x_dataset.getXValue(1, l_subtrahendItem);
                l_y3 = x_dataset.getYValue(1, l_subtrahendItem);
                l_subtrahendCurX = new Double(l_x3);
                l_subtrahendCurY = new Double(l_y3);

                if (!b_subtrahendAtIntersect) {
                    l_subtrahendXs.add(l_subtrahendCurX);
                    l_subtrahendYs.add(l_subtrahendCurY);
                }

                l_subtrahendMaxY = Math.max(l_subtrahendMaxY, l_y3);
                l_subtrahendMinY = Math.min(l_subtrahendMinY, l_y3);

                l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1);
                l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1);
                l_subtrahendNextX = new Double(l_x4);
                l_subtrahendNextY = new Double(l_y4);
            }

            // deassert b_*FastForward (only matters for 1st time through loop)
            b_minuendFastForward = false;
            b_subtrahendFastForward = false;

            Double l_intersectX = null;
            Double l_intersectY = null;
            boolean b_intersect = false;

            b_minuendAtIntersect = false;
            b_subtrahendAtIntersect = false;

            // check for intersect
            if ((l_x2 == l_x4) && (l_y2 == l_y4)) {
                // check if line segments are colinear
                if ((l_x1 == l_x3) && (l_y1 == l_y3)) {
                    b_colinear = true;
                } else {
                    // the intersect is at the next point for both the minuend
                    // and subtrahend
                    l_intersectX = new Double(l_x2);
                    l_intersectY = new Double(l_y2);

                    b_intersect = true;
                    b_minuendAtIntersect = true;
                    b_subtrahendAtIntersect = true;
                }
            } else {
                // compute common denominator
                final double l_denominator = ((l_y4 - l_y3) * (l_x2 - l_x1)) - ((l_x4 - l_x3) * (l_y2 - l_y1));

                // compute common deltas
                final double l_deltaY = l_y1 - l_y3;
                final double l_deltaX = l_x1 - l_x3;

                // compute numerators
                final double l_numeratorA = ((l_x4 - l_x3) * l_deltaY) - ((l_y4 - l_y3) * l_deltaX);
                final double l_numeratorB = ((l_x2 - l_x1) * l_deltaY) - ((l_y2 - l_y1) * l_deltaX);

                // check if line segments are colinear
                if ((0 == l_numeratorA) && (0 == l_numeratorB) && (0 == l_denominator)) {
                    b_colinear = true;
                } else {
                    // check if previously colinear
                    if (b_colinear) {
                        // clear colinear points and flag
                        l_minuendXs.clear();
                        l_minuendYs.clear();
                        l_subtrahendXs.clear();
                        l_subtrahendYs.clear();
                        l_polygonXs.clear();
                        l_polygonYs.clear();

                        b_colinear = false;

                        // set new starting point for the polygon
                        final boolean b_useMinuend = ((l_x3 <= l_x1) && (l_x1 <= l_x4));
                        l_polygonXs.add(b_useMinuend ? l_minuendCurX : l_subtrahendCurX);
                        l_polygonYs.add(b_useMinuend ? l_minuendCurY : l_subtrahendCurY);
                    }

                    // compute slope components
                    final double l_slopeA = l_numeratorA / l_denominator;
                    final double l_slopeB = l_numeratorB / l_denominator;

                    // check if the line segments intersect
                    if ((0 < l_slopeA) && (l_slopeA <= 1) && (0 < l_slopeB) && (l_slopeB <= 1)) {
                        // compute the point of intersection
                        final double l_xi = l_x1 + (l_slopeA * (l_x2 - l_x1));
                        final double l_yi = l_y1 + (l_slopeA * (l_y2 - l_y1));

                        l_intersectX = new Double(l_xi);
                        l_intersectY = new Double(l_yi);
                        b_intersect = true;
                        b_minuendAtIntersect = ((l_xi == l_x2) && (l_yi == l_y2));
                        b_subtrahendAtIntersect = ((l_xi == l_x4) && (l_yi == l_y4));

                        // advance minuend and subtrahend to intesect
                        l_minuendCurX = l_intersectX;
                        l_minuendCurY = l_intersectY;
                        l_subtrahendCurX = l_intersectX;
                        l_subtrahendCurY = l_intersectY;
                    }
                }
            }

            if (b_intersect) {
                // create the polygon
                // add the minuend's points to polygon
                l_polygonXs.addAll(l_minuendXs);
                l_polygonYs.addAll(l_minuendYs);

                // add intersection point to the polygon
                l_polygonXs.add(l_intersectX);
                l_polygonYs.add(l_intersectY);

                // add the subtrahend's points to the polygon in reverse
                Collections.reverse(l_subtrahendXs);
                Collections.reverse(l_subtrahendYs);
                l_polygonXs.addAll(l_subtrahendXs);
                l_polygonYs.addAll(l_subtrahendYs);

                // create an actual polygon
                b_positive = (l_subtrahendMaxY <= l_minuendMaxY) && (l_subtrahendMinY <= l_minuendMinY);
                createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);

                // clear the point vectors
                l_minuendXs.clear();
                l_minuendYs.clear();
                l_subtrahendXs.clear();
                l_subtrahendYs.clear();
                l_polygonXs.clear();
                l_polygonYs.clear();

                // set the maxY and minY values to intersect y-value
                final double l_y = l_intersectY.doubleValue();
                l_minuendMaxY = l_y;
                l_subtrahendMaxY = l_y;
                l_minuendMinY = l_y;
                l_subtrahendMinY = l_y;

                // add interection point to new polygon
                l_polygonXs.add(l_intersectX);
                l_polygonYs.add(l_intersectY);
            }

            // advance the minuend if needed
            if (l_x2 <= l_x4) {
                l_minuendItem++;
                b_minuendAdvanced = true;
            } else {
                b_minuendAdvanced = false;
            }

            // advance the subtrahend if needed
            if (l_x4 <= l_x2) {
                l_subtrahendItem++;
                b_subtrahendAdvanced = true;
            } else {
                b_subtrahendAdvanced = false;
            }

            b_minuendDone = (l_minuendItem == (l_minuendItemCount - 1));
            b_subtrahendDone = (l_subtrahendItem == (l_subtrahendItemCount - 1));
        }

        // check if the final polygon needs to be clipped
        if (b_minuendDone && (l_x3 < l_x2) && (l_x2 < l_x4)) {
            // project onto subtrahend
            final double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3);
            l_subtrahendNextX = l_minuendNextX;
            l_subtrahendNextY = new Double((l_slope * l_x2) + (l_y3 - (l_slope * l_x3)));
        }

        if (b_subtrahendDone && (l_x1 < l_x4) && (l_x4 < l_x2)) {
            // project onto minuend
            final double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1);
            l_minuendNextX = l_subtrahendNextX;
            l_minuendNextY = new Double((l_slope * l_x4) + (l_y1 - (l_slope * l_x1)));
        }

        // consider last point of minuend and subtrahend for determining
        // positivity
        l_minuendMaxY = Math.max(l_minuendMaxY, l_minuendNextY.doubleValue());
        l_subtrahendMaxY = Math.max(l_subtrahendMaxY, l_subtrahendNextY.doubleValue());
        l_minuendMinY = Math.min(l_minuendMinY, l_minuendNextY.doubleValue());
        l_subtrahendMinY = Math.min(l_subtrahendMinY, l_subtrahendNextY.doubleValue());

        // add the last point of the minuned and subtrahend
        l_minuendXs.add(l_minuendNextX);
        l_minuendYs.add(l_minuendNextY);
        l_subtrahendXs.add(l_subtrahendNextX);
        l_subtrahendYs.add(l_subtrahendNextY);

        // create the polygon
        // add the minuend's points to polygon
        l_polygonXs.addAll(l_minuendXs);
        l_polygonYs.addAll(l_minuendYs);

        // add the subtrahend's points to the polygon in reverse
        Collections.reverse(l_subtrahendXs);
        Collections.reverse(l_subtrahendYs);
        l_polygonXs.addAll(l_subtrahendXs);
        l_polygonYs.addAll(l_subtrahendYs);

        // create an actual polygon
        b_positive = (l_subtrahendMaxY <= l_minuendMaxY) && (l_subtrahendMinY <= l_minuendMinY);
        createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);
    }

    private void drawItemPass1(final Graphics2D g2, final Rectangle2D dataArea, final PlotRenderingInfo info, final XYPlot plot, final ValueAxis domainAxis,
            final ValueAxis rangeAxis, final XYDataset dataset, final int series, final int item, final CrosshairState crosshairState) {

        doDrawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState);

        final int lastSeries = dataset.getSeriesCount() - 1;
        final int lastItem = dataset.getItemCount(series) - 1;
        if (series == lastSeries && item == lastItem) {
            // draw labels: only once per theme!
            drawAreaLabel(g2, dataArea, plot, domainAxis, rangeAxis);
            drawTitleLabel(g2, dataArea, info.getOwner().getEntityCollection(), dataset);

            drawZeroBaseline(g2, dataArea, plot, domainAxis, rangeAxis);
        }
    }

    private void drawAreaLabel(final Graphics2D g2, final Rectangle2D dataArea, final XYPlot plot, final ValueAxis domainAxis, final ValueAxis rangeAxis) {

        if (!this.drawAreaLabel)
            return;

        if (this.areaLabelNumberFormat == null || this.areaLabelTamplate == null || this.centroid == null)
            return;

        // Respect text-extend if text should appear really centered.
        float area = 0f;
        if (this.areaCalculationMode == CALCULATE_POSITIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA)
            area += Math.abs(this.positiveArea);
        if (this.areaCalculationMode == CALCULATE_NEGATIVE_AREA || this.areaCalculationMode == CALCULATE_ALL_AREA)
            area += Math.abs(this.negativeArea);

        if (area != 0f) {

            final String labelText = String.format(this.areaLabelTamplate, this.areaLabelNumberFormat.format(area));

            final double center_x = this.centroid.getX();
            final double center_y = this.centroid.getY();

            final double screenX = domainAxis.valueToJava2D(center_x, dataArea, plot.getDomainAxisEdge());
            final double screenY = rangeAxis.valueToJava2D(center_y, dataArea, plot.getRangeAxisEdge());

            drawLabel(g2, labelText, screenX, screenY);
        }
    }

    private void drawTitleLabel(final Graphics2D g2, final Rectangle2D dataArea, final EntityCollection entities, final XYDataset dataset) {

        if (!this.drawTitleLabel)
            return;

        if (Double.isInfinite(this.minimumScreenX))
            return;

        if (dataset instanceof XYSeriesCollection) {
            final XYSeries xYSeries = ((XYSeriesCollection) dataset).getSeries(0);
            final String label = (xYSeries instanceof HasLabel) ? ((HasLabel) xYSeries).getLabel() : xYSeries.getKey().toString();

            EnhancedLineAndShapeRenderer.drawLineLabel(g2, dataArea, entities, this.minimumScreenX, this.minimumScreenX_Y, this.labelFont, this.labelColor,
                    this.labelBGColor != null, this.labelBGColor, label);
        }
    }

    private void drawZeroBaseline(final Graphics2D g2, final Rectangle2D dataArea, final XYPlot plot, final ValueAxis domainAxis, final ValueAxis rangeAxis) {

        if (this.baseLineColor == null)
            return;

        final PlotOrientation orientation = plot.getOrientation();
        final RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
        final RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();

        final Range domainRange = domainAxis.getRange();
        final double x0 = domainRange.getLowerBound();
        final double x1 = domainRange.getUpperBound();

        final double screenX0 = domainAxis.valueToJava2D(x0, dataArea, domainAxisLocation);
        final double screenX1 = domainAxis.valueToJava2D(x1, dataArea, domainAxisLocation);
        final double screenY = rangeAxis.valueToJava2D(0.0, dataArea, rangeAxisLocation);

        Shape baseLine;
        if (PlotOrientation.HORIZONTAL == orientation)
            baseLine = new Line2D.Double(screenY, screenX0, screenY, screenX1);
        else
            baseLine = new Line2D.Double(screenX0, screenY, screenX1, screenY);

        if (baseLine.intersects(dataArea)) {
            g2.setPaint(this.baseLineColor);
            g2.setStroke(new BasicStroke(2));

            g2.draw(baseLine);
        }
    }

    /**
     * Draws the visual representation of a single data item, second pass. In
     * the second pass, the renderer draws the lines and shapes for the
     * individual points in the two series.
     *
     * @param x_graphics
     *            the graphics device.
     * @param x_dataArea
     *            the area within which the data is being drawn.
     * @param x_info
     *            collects information about the drawing.
     * @param x_plot
     *            the plot (can be used to obtain standard color
     *            information etc).
     * @param x_domainAxis
     *            the domain (horizontal) axis.
     * @param x_rangeAxis
     *            the range (vertical) axis.
     * @param x_dataset
     *            the dataset.
     * @param x_series
     *            the series index (zero-based).
     * @param x_item
     *            the item index (zero-based).
     * @param x_crosshairState
     *            crosshair information for the plot
     *            (<code>null</code> permitted).
     */
    private void doDrawItemPass1(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final PlotRenderingInfo x_info, final XYPlot x_plot,
            final ValueAxis x_domainAxis, final ValueAxis x_rangeAxis, final XYDataset x_dataset, final int x_series, final int x_item,
            final CrosshairState x_crosshairState) {

        Shape l_entityArea = null;
        EntityCollection l_entities = null;
        if (x_info != null)
            l_entities = x_info.getOwner().getEntityCollection();

        final Paint l_seriesPaint = getItemPaint(x_series, x_item);
        final Stroke l_seriesStroke = getItemStroke(x_series, x_item);
        x_graphics.setPaint(l_seriesPaint);
        x_graphics.setStroke(l_seriesStroke);

        final PlotOrientation l_orientation = x_plot.getOrientation();
        final RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge();
        final RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge();

        final double l_x0 = x_dataset.getXValue(x_series, x_item);
        final double l_y0 = x_dataset.getYValue(x_series, x_item);
        final double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea, l_domainAxisLocation);
        final double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea, l_rangeAxisLocation);

        /* update minimumScreenX -> used to position title */
        // REMARK: ignore points with y == 0.0 --> else the label sticks on the zero-line, becaue most area themes start at 0.0
        if (l_x1 < this.minimumScreenX && Math.abs(l_y1) > 0.01) {
            this.minimumScreenX = l_x1;
            this.minimumScreenX_Y = l_y1;
        }

        // These are the shapes of the series items.
        if (getShapesVisible()) {
            Shape l_shape = getItemShape(x_series, x_item);
            if (l_orientation == PlotOrientation.HORIZONTAL) {
                l_shape = ShapeUtilities.createTranslatedShape(l_shape, l_y1, l_x1);
            } else {
                l_shape = ShapeUtilities.createTranslatedShape(l_shape, l_x1, l_y1);
            }
            if (l_shape.intersects(x_dataArea)) {
                x_graphics.setPaint(getItemPaint(x_series, x_item));
                x_graphics.fill(l_shape);
                /*
                 * TODO We could draw the shapes of single items here.
                 * if (drawOutline) {
                 * x_graphics.setPaint(this.outlinePaint);
                 * x_graphics.setStroke(this.outlineStroke);
                 * x_graphics.draw(l_shape);
                 * }
                 */
            }
            l_entityArea = l_shape;
        } // if (getShapesVisible())

        // add an entity for the item...
        if (l_entities != null) {
            if (l_entityArea == null)
                l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2), 4, 4);

            String l_tip = null;
            final XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series, x_item);
            if (null != l_tipGenerator) {
                l_tip = l_tipGenerator.generateToolTip(x_dataset, x_series, x_item);
            }
            String l_url = null;
            final XYURLGenerator l_urlGenerator = getURLGenerator();
            if (null != l_urlGenerator) {
                l_url = l_urlGenerator.generateURL(x_dataset, x_series, x_item);
            }
            final XYItemEntity l_entity = new XYItemEntity(l_entityArea, x_dataset, x_series, x_item, l_tip, l_url);
            l_entities.add(l_entity);
        }

        // draw the item label if there is one...
        if (isItemLabelVisible(x_series, x_item))
            drawItemLabel(x_graphics, l_orientation, x_dataset, x_series, x_item, l_x1, l_y1, (l_y1 < 0.0));

        final int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis);
        final int l_rangeAxisIndex = x_plot.getRangeAxisIndex(x_rangeAxis);
        updateCrosshairValues(x_crosshairState, l_x0, l_y0, l_domainAxisIndex, l_rangeAxisIndex, l_x1, l_y1, l_orientation);

        if (0 == x_item) {
            return;
        }

        final double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series, (x_item - 1)), x_dataArea, l_domainAxisLocation);
        final double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, (x_item - 1)), x_dataArea, l_rangeAxisLocation);

        Line2D l_line = null;
        if (PlotOrientation.HORIZONTAL == l_orientation)
            l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2);
        else if (PlotOrientation.VERTICAL == l_orientation)
            l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2);

        if ((null != l_line) && l_line.intersects(x_dataArea)) {
            x_graphics.setPaint(getItemPaint(x_series, x_item));
            x_graphics.setStroke(getItemStroke(x_series, x_item));
            if (this.drawOriginalSeries) {
                x_graphics.setPaint(this.outlinePaint);
                x_graphics.setStroke(this.outlineStroke);
                x_graphics.draw(l_line);
            }
        }
    }

    /**
     * Determines if a dataset is degenerate. A degenerate dataset is a
     * dataset where either series has less than two (2) points.
     *
     * @param x_dataset
     *            the dataset.
     * @param x_impliedZeroSubtrahend
     *            if false, do not check the subtrahend
     *
     * @return true if the dataset is degenerate.
     */
    private boolean isEitherSeriesDegenerate(final XYDataset x_dataset, final boolean x_impliedZeroSubtrahend) {

        if (x_impliedZeroSubtrahend) {
            return (x_dataset.getItemCount(0) < 2);
        }

        return ((x_dataset.getItemCount(0) < 2) || (x_dataset.getItemCount(1) < 2));
    }

    /**
     * Determines if the two (2) series are disjoint.
     * Disjoint series do not overlap in the domain space.
     *
     * @param x_dataset
     *            the dataset.
     *
     * @return true if the dataset is degenerate.
     */
    private boolean areSeriesDisjoint(final XYDataset x_dataset) {

        final int l_minuendItemCount = x_dataset.getItemCount(0);
        final double l_minuendFirst = x_dataset.getXValue(0, 0);
        final double l_minuendLast = x_dataset.getXValue(0, l_minuendItemCount - 1);

        final int l_subtrahendItemCount = x_dataset.getItemCount(1);
        final double l_subtrahendFirst = x_dataset.getXValue(1, 0);
        final double l_subtrahendLast = x_dataset.getXValue(1, l_subtrahendItemCount - 1);

        return ((l_minuendLast < l_subtrahendFirst) || (l_subtrahendLast < l_minuendFirst));
    }

    private void updateCentroid(final Object[] xValues, final Object[] yValues) {
        double x = 0d, y = 0d;

        for (int i = 0, N = xValues.length; i < N; ++i) {
            x += ((Double) xValues[i]).doubleValue();
            y += ((Double) yValues[i]).doubleValue();
        }

        x /= xValues.length;
        y /= yValues.length;

        this.centroidNPoints++;
        final double factorNew = 1d / this.centroidNPoints;
        final double factorOld = 1d - factorNew;

        this.centroid = new Point2D.Double((factorNew * x + factorOld * this.centroid.x), (factorNew * y + factorOld * this.centroid.y));
    }

    private static double calculateArea(final Object[] xValues, final Object[] yValues) {
        double area = 0d;

        for (int i = 0, N = xValues.length; i < N; ++i) {
            final int k = (i + 1) % N;
            final double xi = ((Double) xValues[i]).doubleValue();
            final double yi = ((Double) yValues[i]).doubleValue();
            final double xk = ((Double) xValues[k]).doubleValue();
            final double yk = ((Double) yValues[k]).doubleValue();

            area += xi * yk;
            area -= xk * yi;
            // TODO centroid calculation here?
        }

        return 0.5d * area;
    }

    /**
     * Draws the visual representation of a polygon
     *
     * @param x_graphics
     *            the graphics device.
     * @param x_dataArea
     *            the area within which the data is being drawn.
     * @param x_plot
     *            the plot (can be used to obtain standard color
     *            information etc).
     * @param x_domainAxis
     *            the domain (horizontal) axis.
     * @param x_rangeAxis
     *            the range (vertical) axis.
     * @param x_positive
     *            indicates if the polygon is positive (true) or
     *            negative (false).
     * @param x_xValues
     *            a linked list of the x values (expects values to be
     *            of type Double).
     * @param x_yValues
     *            a linked list of the y values (expects values to be
     *            of type Double).
     */
    private void createPolygon(final Graphics2D x_graphics, final Rectangle2D x_dataArea, final XYPlot x_plot, final ValueAxis x_domainAxis,
            final ValueAxis x_rangeAxis, final boolean x_positive, final List<Double> x_xValues, final List<Double> x_yValues) {

        final PlotOrientation l_orientation = x_plot.getOrientation();
        final RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge();
        final RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge();

        final Object[] l_xValues = x_xValues.toArray();
        final Object[] l_yValues = x_yValues.toArray();

        final double area = calculateArea(l_xValues, l_yValues);
        if (x_positive)
            this.positiveArea += area;
        else
            this.negativeArea += area;
        updateCentroid(l_xValues, l_yValues);

        final GeneralPath l_path = new GeneralPath();

        if (PlotOrientation.VERTICAL == l_orientation) {
            double l_x = x_domainAxis.valueToJava2D(((Double) l_xValues[0]).doubleValue(), x_dataArea, l_domainAxisLocation);
            if (this.roundXCoordinates) {
                l_x = Math.rint(l_x);
            }

            double l_y = x_rangeAxis.valueToJava2D(((Double) l_yValues[0]).doubleValue(), x_dataArea, l_rangeAxisLocation);

            l_path.moveTo((float) l_x, (float) l_y);
            for (int i = 1; i < l_xValues.length; i++) {
                l_x = x_domainAxis.valueToJava2D(((Double) l_xValues[i]).doubleValue(), x_dataArea, l_domainAxisLocation);
                if (this.roundXCoordinates) {
                    l_x = Math.rint(l_x);
                }

                l_y = x_rangeAxis.valueToJava2D(((Double) l_yValues[i]).doubleValue(), x_dataArea, l_rangeAxisLocation);
                l_path.lineTo((float) l_x, (float) l_y);
            }
            l_path.closePath();
        } else {
            double l_x = x_domainAxis.valueToJava2D(((Double) l_xValues[0]).doubleValue(), x_dataArea, l_domainAxisLocation);
            if (this.roundXCoordinates) {
                l_x = Math.rint(l_x);
            }

            double l_y = x_rangeAxis.valueToJava2D(((Double) l_yValues[0]).doubleValue(), x_dataArea, l_rangeAxisLocation);

            l_path.moveTo((float) l_y, (float) l_x);
            for (int i = 1; i < l_xValues.length; i++) {
                l_x = x_domainAxis.valueToJava2D(((Double) l_xValues[i]).doubleValue(), x_dataArea, l_domainAxisLocation);
                if (this.roundXCoordinates) {
                    l_x = Math.rint(l_x);
                }

                l_y = x_rangeAxis.valueToJava2D(((Double) l_yValues[i]).doubleValue(), x_dataArea, l_rangeAxisLocation);
                l_path.lineTo((float) l_y, (float) l_x);
            }
            l_path.closePath();
        }

        if (l_path.intersects(x_dataArea)) {

            final Paint paint = x_positive ? getPositivePaint() : getNegativePaint();
            if (this.drawArea && paint != null) {
                x_graphics.setPaint(paint);
                x_graphics.fill(l_path);
            }

            if (this.drawOutline) {
                x_graphics.setStroke(this.outlineStroke);
                x_graphics.setPaint(this.outlinePaint);
                x_graphics.draw(l_path);
            }
        }
    }

    /**
     * Returns a default legend item for the specified series. Subclasses
     * should override this method to generate customised items.
     *
     * @param datasetIndex
     *            the dataset index (zero-based).
     * @param series
     *            the series index (zero-based).
     *
     * @return A legend item for the series.
     */
    @Override
    public LegendItem getLegendItem(final int datasetIndex, final int series) {
        final XYPlot p = getPlot();
        if (p == null)
            return null;

        final XYDataset dataset = p.getDataset(datasetIndex);
        if (dataset == null)
            return null;

        if (!getItemVisible(series, 0))
            return null;

        final String label = getLegendItemLabelGenerator().generateLabel(dataset, series);
        final String description = label;
        String toolTipText = null;
        if (getLegendItemToolTipGenerator() != null) {
            toolTipText = getLegendItemToolTipGenerator().generateLabel(dataset, series);
        }
        String urlText = null;
        if (getLegendItemURLGenerator() != null) {
            urlText = getLegendItemURLGenerator().generateLabel(dataset, series);
        }

        // "Area-Style"- Paint.
        final Paint paint = getPositivePaint();
        final Shape line = getLegendLine();

        // Filled Shape ("Area-Style").
        final BasicStroke NULL_STROKE = new BasicStroke(0.0f);
        final Shape NULL_SHAPE = new Line2D.Float();

        final Paint outlPaint = this.drawOutline ? this.outlinePaint : Color.black;
        final Stroke outlStroke = this.drawOutline ? this.outlineStroke : NULL_STROKE;

        final LegendItem result = new LegendItem(label, description, toolTipText, urlText, true, line, this.drawArea, paint, this.drawOutline, outlPaint,
                outlStroke, false, NULL_SHAPE, NULL_STROKE, Color.black);

        result.setLabelFont(lookupLegendTextFont(series));
        final Paint labelPaint = lookupLegendTextPaint(series);
        if (labelPaint != null)
            result.setLabelPaint(labelPaint);
        result.setDataset(dataset);
        result.setDatasetIndex(datasetIndex);
        result.setSeriesKey(dataset.getSeriesKey(series));
        result.setSeriesIndex(series);

        return result;
    }

    /**
     * Tests this renderer for equality with an arbitrary object.
     *
     * @param obj
     *            the object (<code>null</code> permitted).
     *
     * @return A boolean.
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof StableXYDifferenceRenderer)) {
            return false;
        }
        if (!super.equals(obj)) {
            return false;
        }
        final StableXYDifferenceRenderer that = (StableXYDifferenceRenderer) obj;
        if (!PaintUtilities.equal(this.positivePaint, that.positivePaint)) {
            return false;
        }
        if (!PaintUtilities.equal(this.negativePaint, that.negativePaint)) {
            return false;
        }
        if (this.shapesVisible != that.shapesVisible) {
            return false;
        }
        if (!ShapeUtilities.equal(this.legendShape, that.legendShape)) {
            return false;
        }
        if (this.roundXCoordinates != that.roundXCoordinates) {
            return false;
        }
        return true;
    }

    /**
     * Returns a clone of the renderer.
     *
     * @return A clone.
     *
     * @throws CloneNotSupportedException
     *             if the renderer cannot be cloned.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        final StableXYDifferenceRenderer clone = (StableXYDifferenceRenderer) super.clone();
        clone.legendShape = ShapeUtilities.clone(this.legendShape);
        return clone;
    }

    /**
     * Provides serialization support.
     *
     * @param stream
     *            the output stream.
     *
     * @throws IOException
     *             if there is an I/O error.
     */
    private void writeObject(final ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
        SerialUtilities.writePaint(this.positivePaint, stream);
        SerialUtilities.writePaint(this.negativePaint, stream);
        SerialUtilities.writeShape(this.legendShape, stream);
    }

    /**
     * Provides serialization support.
     *
     * @param stream
     *            the input stream.
     *
     * @throws IOException
     *             if there is an I/O error.
     * @throws ClassNotFoundException
     *             if there is a classpath problem.
     */
    private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        this.positivePaint = SerialUtilities.readPaint(stream);
        this.negativePaint = SerialUtilities.readPaint(stream);
        this.legendShape = SerialUtilities.readShape(stream);
    }

    private void drawLabel(final Graphics2D g2, final String labelText, final double screenX, final double screenY) {

        final Color oldColor = g2.getColor();
        final Font oldFont = g2.getFont();

        g2.setFont(this.labelFont);
        if (this.labelBGColor != null)
            EnhancedLineAndShapeRenderer.drawTextBox(g2, labelText, (float) screenX, (float) screenY, this.labelBGColor);

        g2.setColor(this.labelColor);
        g2.drawString(labelText, (float) screenX, (float) screenY);

        g2.setFont(oldFont);
        g2.setColor(oldColor);
    }
}

http://dive4elements.wald.intevation.org