    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * -------------------------
    028     * XYDifferenceRenderer.java
    029     * -------------------------
    030     * (C) Copyright 2003-2007, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Christian W. Zuckschwerdt;
    034     *
    035     * $Id: XYDifferenceRenderer.java,v 1.12.2.10 2007/03/08 17:22:53 mungady Exp $
    036     *
    037     * Changes:
    038     * --------
    039     * 30-Apr-2003 : Version 1 (DG);
    040     * 30-Jul-2003 : Modified entity constructor (CZ);
    041     * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
    042     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
    043     * 09-Feb-2004 : Updated to support horizontal plot orientation (DG);
    044     * 10-Feb-2004 : Added default constructor, setter methods and updated
    045     *               Javadocs (DG);
    046     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
    047     * 30-Mar-2004 : Fixed bug in getNegativePaint() method (DG);
    048     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    049     *               getYValue() (DG);
    050     * 25-Aug-2004 : Fixed a bug preventing the use of crosshairs (DG);
    051     * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
    052     * 19-Jan-2005 : Now accesses only primitive values from dataset (DG);
    053     * 22-Feb-2005 : Override getLegendItem(int, int) to return "line" items (DG);
    054     * 13-Apr-2005 : Fixed shape positioning bug (id = 1182062) (DG);
    055     * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG);
    056     * 04-May-2005 : Override equals() method, renamed get/setPlotShapes() -->
    057     *               get/setShapesVisible (DG);
    058     * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG);
    059     * 16-Jun-2005 : Fix bug (1221021) affecting stroke used for each series (DG);
    060     * ------------- JFREECHART 1.0.x ---------------------------------------------
    061     * 24-Jan-2007 : Added flag to allow rounding of x-coordinates, and fixed
    062     *               bug in clone() (DG);
    063     * 05-Feb-2007 : Added an extra call to updateCrosshairValues() in
    064     *               drawItemPass1(), to fix bug 1564967 (DG);
    065     * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
    066     * 08-Mar-2007 : Fixed entity generation (DG);
    067     *
    068     */
    069
    070    package org.jfree.chart.renderer.xy;
    071
    072    import java.awt.Color;
    073    import java.awt.Graphics2D;
    074    import java.awt.Paint;
    075    import java.awt.Shape;
    076    import java.awt.Stroke;
    077    import java.awt.geom.GeneralPath;
    078    import java.awt.geom.Line2D;
    079    import java.awt.geom.Rectangle2D;
    080    import java.io.IOException;
    081    import java.io.ObjectInputStream;
    082    import java.io.ObjectOutputStream;
    083    import java.io.Serializable;
    084
    085    import org.jfree.chart.LegendItem;
    086    import org.jfree.chart.axis.ValueAxis;
    087    import org.jfree.chart.entity.EntityCollection;
    088    import org.jfree.chart.entity.XYItemEntity;
    089    import org.jfree.chart.event.RendererChangeEvent;
    090    import org.jfree.chart.labels.XYToolTipGenerator;
    091    import org.jfree.chart.plot.CrosshairState;
    092    import org.jfree.chart.plot.PlotOrientation;
    093    import org.jfree.chart.plot.PlotRenderingInfo;
    094    import org.jfree.chart.plot.XYPlot;
    095    import org.jfree.data.xy.XYDataset;
    096    import org.jfree.io.SerialUtilities;
    097    import org.jfree.ui.RectangleEdge;
    098    import org.jfree.util.PaintUtilities;
    099    import org.jfree.util.PublicCloneable;
    100    import org.jfree.util.ShapeUtilities;
    101
    102    /**
    103     * A renderer for an {@link XYPlot} that highlights the differences between two
    104     * series.  The renderer expects a dataset that:
    105     * <ul>
    106     * <li>has exactly two series;</li>
    107     * <li>each series has the same x-values;</li>
    108     * <li>no <code>null</code> values;
    109     * </ul>
    110     */
    111    public class XYDifferenceRenderer extends AbstractXYItemRenderer
    112                                      implements XYItemRenderer,
    113                                                 Cloneable,
    114                                                 PublicCloneable,
    115                                                 Serializable {
    116
    117        /** For serialization. */
    118        private static final long serialVersionUID = -8447915602375584857L;
    119
    120        /** The paint used to highlight positive differences (y(0) > y(1)). */
    121        private transient Paint positivePaint;
    122
    123        /** The paint used to highlight negative differences (y(0) < y(1)). */
    124        private transient Paint negativePaint;
    125
    126        /** Display shapes at each point? */
    127        private boolean shapesVisible;
    128
    129        /** The shape to display in the legend item. */
    130        private transient Shape legendLine;
    131
    132        /**
    133         * This flag controls whether or not the x-coordinates (in Java2D space)
    134         * are rounded to integers.  When set to true, this can avoid the vertical
    135         * striping that anti-aliasing can generate.  However, the rounding may not
    136         * be appropriate for output in high resolution formats (for example,
    137         * vector graphics formats such as SVG and PDF).
    138         *
    139         * @since 1.0.4
    140         */
    141        private boolean roundXCoordinates;
    142
    143        /**
    144         * Creates a new renderer with default attributes.
    145         */
    146        public XYDifferenceRenderer() {
    147            this(Color.green, Color.red, false);
    148        }
    149
    150        /**
    151         * Creates a new renderer.
    152         *
    153         * @param positivePaint  the highlight color for positive differences
    154         *                       (<code>null</code> not permitted).
    155         * @param negativePaint  the highlight color for negative differences
    156         *                       (<code>null</code> not permitted).
    157         * @param shapes  draw shapes?
    158         */
    159        public XYDifferenceRenderer(Paint positivePaint, Paint negativePaint,
    160                                    boolean shapes) {
    161            if (positivePaint == null) {
    162                throw new IllegalArgumentException(
    163                        "Null 'positivePaint' argument.");
    164            }
    165            if (negativePaint == null) {
    166                throw new IllegalArgumentException(
    167                        "Null 'negativePaint' argument.");
    168            }
    169            this.positivePaint = positivePaint;
    170            this.negativePaint = negativePaint;
    171            this.shapesVisible = shapes;
    172            this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
    173            this.roundXCoordinates = false;
    174        }
    175
    176        /**
    177         * Returns the paint used to highlight positive differences.
    178         *
    179         * @return The paint (never <code>null</code>).
    180         *
    181         * @see #setPositivePaint(Paint)
    182         */
    183        public Paint getPositivePaint() {
    184            return this.positivePaint;
    185        }
    186
    187        /**
    188         * Sets the paint used to highlight positive differences.
    189         *
    190         * @param paint  the paint (<code>null</code> not permitted).
    191         *
    192         * @see #getPositivePaint()
    193         */
    194        public void setPositivePaint(Paint paint) {
    195            if (paint == null) {
    196                throw new IllegalArgumentException("Null 'paint' argument.");
    197            }
    198            this.positivePaint = paint;
    199            notifyListeners(new RendererChangeEvent(this));
    200        }
    201
    202        /**
    203         * Returns the paint used to highlight negative differences.
    204         *
    205         * @return The paint (never <code>null</code>).
    206         *
    207         * @see #setNegativePaint(Paint)
    208         */
    209        public Paint getNegativePaint() {
    210            return this.negativePaint;
    211        }
    212
    213        /**
    214         * Sets the paint used to highlight negative differences.
    215         *
    216         * @param paint  the paint (<code>null</code> not permitted).
    217         *
    218         * @see #getNegativePaint()
    219         */
    220        public void setNegativePaint(Paint paint) {
    221            if (paint == null) {
    222                throw new IllegalArgumentException("Null 'paint' argument.");
    223            }
    224            this.negativePaint = paint;
    225            notifyListeners(new RendererChangeEvent(this));
    226        }
    227
    228        /**
    229         * Returns a flag that controls whether or not shapes are drawn for each
    230         * data value.
    231         *
    232         * @return A boolean.
    233         *
    234         * @see #setShapesVisible(boolean)
    235         */
    236        public boolean getShapesVisible() {
    237            return this.shapesVisible;
    238        }
    239
    240        /**
    241         * Sets a flag that controls whether or not shapes are drawn for each
    242         * data value.
    243         *
    244         * @param flag  the flag.
    245         *
    246         * @see #getShapesVisible()
    247         */
    248        public void setShapesVisible(boolean flag) {
    249            this.shapesVisible = flag;
    250            notifyListeners(new RendererChangeEvent(this));
    251        }
    252
    253        /**
    254         * Returns the shape used to represent a line in the legend.
    255         *
    256         * @return The legend line (never <code>null</code>).
    257         *
    258         * @see #setLegendLine(Shape)
    259         */
    260        public Shape getLegendLine() {
    261            return this.legendLine;
    262        }
    263
    264        /**
    265         * Sets the shape used as a line in each legend item and sends a
    266         * {@link RendererChangeEvent} to all registered listeners.
    267         *
    268         * @param line  the line (<code>null</code> not permitted).
    269         *
    270         * @see #getLegendLine()
    271         */
    272        public void setLegendLine(Shape line) {
    273            if (line == null) {
    274                throw new IllegalArgumentException("Null 'line' argument.");
    275            }
    276            this.legendLine = line;
    277            notifyListeners(new RendererChangeEvent(this));
    278        }
    279
    280        /**
    281         * Returns the flag that controls whether or not the x-coordinates (in
    282         * Java2D space) are rounded to integer values.
    283         *
    284         * @return The flag.
    285         *
    286         * @since 1.0.4
    287         *
    288         * @see #setRoundXCoordinates(boolean)
    289         */
    290        public boolean getRoundXCoordinates() {
    291            return this.roundXCoordinates;
    292        }
    293
    294        /**
    295         * Sets the flag that controls whether or not the x-coordinates (in
    296         * Java2D space) are rounded to integer values, and sends a
    297         * {@link RendererChangeEvent} to all registered listeners.
    298         *
    299         * @param round  the new flag value.
    300         *
    301         * @since 1.0.4
    302         *
    303         * @see #getRoundXCoordinates()
    304         */
    305        public void setRoundXCoordinates(boolean round) {
    306            this.roundXCoordinates = round;
    307            notifyListeners(new RendererChangeEvent(this));
    308        }
    309
    310        /**
    311         * Initialises the renderer and returns a state object that should be
    312         * passed to subsequent calls to the drawItem() method.  This method will
    313         * be called before the first item is rendered, giving the renderer an
    314         * opportunity to initialise any state information it wants to maintain.
    315         * The renderer can do nothing if it chooses.
    316         *
    317         * @param g2  the graphics device.
    318         * @param dataArea  the area inside the axes.
    319         * @param plot  the plot.
    320         * @param data  the data.
    321         * @param info  an optional info collection object to return data back to
    322         *              the caller.
    323         *
    324         * @return A state object.
    325         */
    326        public XYItemRendererState initialise(Graphics2D g2,
    327                                              Rectangle2D dataArea,
    328                                              XYPlot plot,
    329                                              XYDataset data,
    330                                              PlotRenderingInfo info) {
    331
    332            return super.initialise(g2, dataArea, plot, data, info);
    333
    334        }
    335
    336        /**
    337         * Returns <code>2</code>, the number of passes required by the renderer.
    338         * The {@link XYPlot} will run through the dataset this number of times.
    339         *
    340         * @return The number of passes required by the renderer.
    341         */
    342        public int getPassCount() {
    343            return 2;
    344        }
    345
    346        /**
    347         * Draws the visual representation of a single data item.
    348         *
    349         * @param g2  the graphics device.
    350         * @param state  the renderer state.
    351         * @param dataArea  the area within which the data is being drawn.
    352         * @param info  collects information about the drawing.
    353         * @param plot  the plot (can be used to obtain standard color
    354         *              information etc).
    355         * @param domainAxis  the domain (horizontal) axis.
    356         * @param rangeAxis  the range (vertical) axis.
    357         * @param dataset  the dataset.
    358         * @param series  the series index (zero-based).
    359         * @param item  the item index (zero-based).
    360         * @param crosshairState  crosshair information for the plot
    361         *                        (<code>null</code> permitted).
    362         * @param pass  the pass index.
    363         */
    364        public void drawItem(Graphics2D g2,
    365                             XYItemRendererState state,
    366                             Rectangle2D dataArea,
    367                             PlotRenderingInfo info,
    368                             XYPlot plot,
    369                             ValueAxis domainAxis,
    370                             ValueAxis rangeAxis,
    371                             XYDataset dataset,
    372                             int series,
    373                             int item,
    374                             CrosshairState crosshairState,
    375                             int pass) {
    376
    377            if (pass == 0) {
    378                drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis,
    379                        dataset, series, item, crosshairState);
    380            }
    381            else if (pass == 1) {
    382                drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis,
    383                        dataset, series, item, crosshairState);
    384            }
    385
    386        }
    387
    388        /**
    389         * Draws the visual representation of a single data item, first pass.
    390         *
    391         * @param g2  the graphics device.
    392         * @param dataArea  the area within which the data is being drawn.
    393         * @param info  collects information about the drawing.
    394         * @param plot  the plot (can be used to obtain standard color
    395         *              information etc).
    396         * @param domainAxis  the domain (horizontal) axis.
    397         * @param rangeAxis  the range (vertical) axis.
    398         * @param dataset  the dataset.
    399         * @param series  the series index (zero-based).
    400         * @param item  the item index (zero-based).
    401         * @param crosshairState  crosshair information for the plot
    402         *                        (<code>null</code> permitted).
    403         */
    404        protected void drawItemPass0(Graphics2D g2,
    405                                     Rectangle2D dataArea,
    406                                     PlotRenderingInfo info,
    407                                     XYPlot plot,
    408                                     ValueAxis domainAxis,
    409                                     ValueAxis rangeAxis,
    410                                     XYDataset dataset,
    411                                     int series,
    412                                     int item,
    413                                     CrosshairState crosshairState) {
    414
    415            if (series == 0) {
    416
    417                PlotOrientation orientation = plot.getOrientation();
    418                RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
    419                RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
    420
    421                double y0 = dataset.getYValue(0, item);
    422                double x1 = dataset.getXValue(1, item);
    423                double y1 = dataset.getYValue(1, item);
    424
    425                double transY0 = rangeAxis.valueToJava2D(y0, dataArea,
    426                        rangeAxisLocation);
    427                double transX1 = domainAxis.valueToJava2D(x1, dataArea,
    428                        domainAxisLocation);
    429                if (this.roundXCoordinates) {
    430                    transX1 = Math.rint(transX1);
    431                }
    432                double transY1 = rangeAxis.valueToJava2D(y1, dataArea,
    433                        rangeAxisLocation);
    434
    435                if (item > 0) {
    436                    double prevx0 = dataset.getXValue(0, item - 1);
    437                    double prevy0 = dataset.getYValue(0, item - 1);
    438                    double prevy1 = dataset.getYValue(1, item - 1);
    439
    440                    double prevtransX0 = domainAxis.valueToJava2D(prevx0, dataArea,
    441                            domainAxisLocation);
    442                    if (this.roundXCoordinates) {
    443                        prevtransX0 = Math.rint(prevtransX0);
    444                    }
    445                    double prevtransY0 = rangeAxis.valueToJava2D(prevy0, dataArea,
    446                            rangeAxisLocation);
    447                    double prevtransY1 = rangeAxis.valueToJava2D(prevy1, dataArea,
    448                            rangeAxisLocation);
    449
    450                    Shape positive = getPositiveArea((float) prevtransX0,
    451                            (float) prevtransY0, (float) prevtransY1,
    452                            (float) transX1, (float) transY0, (float) transY1,
    453                            orientation);
    454                    if (positive != null) {
    455                        g2.setPaint(getPositivePaint());
    456                        g2.fill(positive);
    457                    }
    458
    459                    Shape negative = getNegativeArea((float) prevtransX0,
    460                            (float) prevtransY0, (float) prevtransY1,
    461                            (float) transX1, (float) transY0, (float) transY1,
    462                            orientation);
    463
    464                    if (negative != null) {
    465                        g2.setPaint(getNegativePaint());
    466                        g2.fill(negative);
    467                    }
    468                }
    469            }
    470
    471        }
    472
    473        /**
    474         * Draws the visual representation of a single data item, second pass.  In
    475         * the second pass, the renderer draws the lines and shapes for the
    476         * individual points in the two series.
    477         *
    478         * @param g2  the graphics device.
    479         * @param dataArea  the area within which the data is being drawn.
    480         * @param info  collects information about the drawing.
    481         * @param plot  the plot (can be used to obtain standard color information
    482         *              etc).
    483         * @param domainAxis  the domain (horizontal) axis.
    484         * @param rangeAxis  the range (vertical) axis.
    485         * @param dataset  the dataset.
    486         * @param series  the series index (zero-based).
    487         * @param item  the item index (zero-based).
    488         * @param crosshairState  crosshair information for the plot
    489         *                        (<code>null</code> permitted).
    490         */
    491        protected void drawItemPass1(Graphics2D g2,
    492                                     Rectangle2D dataArea,
    493                                     PlotRenderingInfo info,
    494                                     XYPlot plot,
    495                                     ValueAxis domainAxis,
    496                                     ValueAxis rangeAxis,
    497                                     XYDataset dataset,
    498                                     int series,
    499                                     int item,
    500                                     CrosshairState crosshairState) {
    501
    502            Shape entityArea0 = null;
    503            Shape entityArea1 = null;
    504            EntityCollection entities = null;
    505            if (info != null) {
    506                entities = info.getOwner().getEntityCollection();
    507            }
    508
    509            Paint seriesPaint = getItemPaint(series, item);
    510            Stroke seriesStroke = getItemStroke(series, item);
    511            g2.setPaint(seriesPaint);
    512            g2.setStroke(seriesStroke);
    513
    514            if (series == 0) {
    515
    516                PlotOrientation orientation = plot.getOrientation();
    517                RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
    518                RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
    519
    520                double x0 = dataset.getXValue(0, item);
    521                double y0 = dataset.getYValue(0, item);
    522                double x1 = dataset.getXValue(1, item);
    523                double y1 = dataset.getYValue(1, item);
    524
    525                double transX0 = domainAxis.valueToJava2D(x0, dataArea,
    526                        domainAxisLocation);
    527                double transY0 = rangeAxis.valueToJava2D(y0, dataArea,
    528                        rangeAxisLocation);
    529                double transX1 = domainAxis.valueToJava2D(x1, dataArea,
    530                        domainAxisLocation);
    531                double transY1 = rangeAxis.valueToJava2D(y1, dataArea,
    532                        rangeAxisLocation);
    533
    534                if (item > 0) {
    535                    // get the previous data points...
    536                    double prevx0 = dataset.getXValue(0, item - 1);
    537                    double prevy0 = dataset.getYValue(0, item - 1);
    538                    double prevx1 = dataset.getXValue(1, item - 1);
    539                    double prevy1 = dataset.getYValue(1, item - 1);
    540
    541                    double prevtransX0 = domainAxis.valueToJava2D(prevx0, dataArea,
    542                            domainAxisLocation);
    543                    double prevtransY0 = rangeAxis.valueToJava2D(prevy0, dataArea,
    544                            rangeAxisLocation);
    545                    double prevtransX1 = domainAxis.valueToJava2D(prevx1, dataArea,
    546                            domainAxisLocation);
    547                    double prevtransY1 = rangeAxis.valueToJava2D(prevy1, dataArea,
    548                            rangeAxisLocation);
    549
    550                    Line2D line0 = null;
    551                    Line2D line1 = null;
    552                    if (orientation == PlotOrientation.HORIZONTAL) {
    553                        line0 = new Line2D.Double(transY0, transX0, prevtransY0,
    554                                prevtransX0);
    555                        line1 = new Line2D.Double(transY1, transX1, prevtransY1,
    556                                prevtransX1);
    557                    }
    558                    else if (orientation == PlotOrientation.VERTICAL) {
    559                        line0 = new Line2D.Double(transX0, transY0, prevtransX0,
    560                                prevtransY0);
    561                        line1 = new Line2D.Double(transX1, transY1, prevtransX1,
    562                                prevtransY1);
    563                    }
    564                    if (line0 != null && line0.intersects(dataArea)) {
    565                        g2.setPaint(getItemPaint(series, item));
    566                        g2.setStroke(getItemStroke(series, item));
    567                        g2.draw(line0);
    568                    }
    569                    if (line1 != null && line1.intersects(dataArea)) {
    570                        g2.setPaint(getItemPaint(1, item));
    571                        g2.setStroke(getItemStroke(1, item));
    572                        g2.draw(line1);
    573                    }
    574                }
    575
    576                if (getShapesVisible()) {
    577                    Shape shape0 = getItemShape(series, item);
    578                    if (orientation == PlotOrientation.HORIZONTAL) {
    579                        shape0 = ShapeUtilities.createTranslatedShape(shape0,
    580                                transY0, transX0);
    581                    }
    582                    else {  // vertical
    583                        shape0 = ShapeUtilities.createTranslatedShape(shape0,
    584                                transX0, transY0);
    585                    }
    586                    if (shape0.intersects(dataArea)) {
    587                        g2.setPaint(getItemPaint(series, item));
    588                        g2.fill(shape0);
    589                    }
    590                    entityArea0 = shape0;
    591
    592                    Shape shape1 = getItemShape(series + 1, item);
    593                    if (orientation == PlotOrientation.HORIZONTAL) {
    594                        shape1 = ShapeUtilities.createTranslatedShape(shape1,
    595                                transY1, transX1);
    596                    }
    597                    else {  // vertical
    598                        shape1 = ShapeUtilities.createTranslatedShape(shape1,
    599                                transX1, transY1);
    600                    }
    601                    if (shape1.intersects(dataArea)) {
    602                        g2.setPaint(getItemPaint(series + 1, item));
    603                        g2.fill(shape1);
    604                    }
    605                    entityArea1 = shape1;
    606
    607                }
    608
    609                // add an entity for the item...
    610                if (entities != null) {
    611                    if (entityArea0 == null) {
    612                        entityArea0 = new Rectangle2D.Double(transX0 - 2,
    613                                transY0 - 2, 4, 4);
    614                    }
    615                    String tip = null;
    616                    XYToolTipGenerator generator = getToolTipGenerator(series,
    617                            item);
    618                    if (generator != null) {
    619                        tip = generator.generateToolTip(dataset, series, item);
    620                    }
    621                    String url = null;
    622                    if (getURLGenerator() != null) {
    623                        url = getURLGenerator().generateURL(dataset, series,
    624                                item);
    625                    }
    626                    XYItemEntity entity = new XYItemEntity(entityArea0, dataset,
    627                            series, item, tip, url);
    628                    entities.add(entity);
    629
    630                    // add an entity for the second item...
    631                    if (entityArea1 == null) {
    632                        entityArea1 = new Rectangle2D.Double(transX1 - 2,
    633                                transY1 - 2, 4, 4);
    634                    }
    635                    tip = null;
    636                    generator = getToolTipGenerator(series, item);
    637                    if (generator != null) {
    638                        tip = generator.generateToolTip(dataset, series + 1, item);
    639                    }
    640                    url = null;
    641                    if (getURLGenerator() != null) {
    642                        url = getURLGenerator().generateURL(dataset,
    643                                series + 1, item);
    644                    }
    645                    entity = new XYItemEntity(entityArea1, dataset,
    646                            series + 1, item, tip, url);
    647                    entities.add(entity);
    648                }
    649
    650                int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
    651                int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
    652                updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex,
    653                        rangeAxisIndex, transX1, transY1, orientation);
    654                updateCrosshairValues(crosshairState, x0, y0, domainAxisIndex,
    655                        rangeAxisIndex, transX0, transY0, orientation);
    656            }
    657
    658        }
    659
    660        /**
    661         * Returns the positive area for a crossover point.
    662         *
    663         * @param x0  x coordinate.
    664         * @param y0A  y coordinate A.
    665         * @param y0B  y coordinate B.
    666         * @param x1  x coordinate.
    667         * @param y1A  y coordinate A.
    668         * @param y1B  y coordinate B.
    669         * @param orientation  the plot orientation.
    670         *
    671         * @return The positive area.
    672         */
    673        protected Shape getPositiveArea(float x0, float y0A, float y0B,
    674                                        float x1, float y1A, float y1B,
    675                                        PlotOrientation orientation) {
    676
    677            Shape result = null;
    678
    679            boolean startsNegative = (y0A >= y0B);
    680            boolean endsNegative = (y1A >= y1B);
    681            if (orientation == PlotOrientation.HORIZONTAL) {
    682                startsNegative = (y0B >= y0A);
    683                endsNegative = (y1B >= y1A);
    684            }
    685
    686            if (startsNegative) {  // starts negative
    687                if (endsNegative) {
    688                    // all negative - return null
    689                    result = null;
    690                }
    691                else {
    692                    // changed from negative to positive
    693                    float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B);
    694                    GeneralPath area = new GeneralPath();
    695                    if (orientation == PlotOrientation.HORIZONTAL) {
    696                        area.moveTo(y1A, x1);
    697                        area.lineTo(p[1], p[0]);
    698                        area.lineTo(y1B, x1);
    699                        area.closePath();
    700                    }
    701                    else if (orientation == PlotOrientation.VERTICAL) {
    702                        area.moveTo(x1, y1A);
    703                        area.lineTo(p[0], p[1]);
    704                        area.lineTo(x1, y1B);
    705                        area.closePath();
    706                    }
    707                    result = area;
    708                }
    709            }
    710            else {  // starts positive
    711                if (endsNegative) {
    712                    // changed from positive to negative
    713                    float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B);
    714                    GeneralPath area = new GeneralPath();
    715                    if (orientation == PlotOrientation.HORIZONTAL) {
    716                        area.moveTo(y0A, x0);
    717                        area.lineTo(p[1], p[0]);
    718                        area.lineTo(y0B, x0);
    719                        area.closePath();
    720                    }
    721                    else if (orientation == PlotOrientation.VERTICAL) {
    722                        area.moveTo(x0, y0A);
    723                        area.lineTo(p[0], p[1]);
    724                        area.lineTo(x0, y0B);
    725                        area.closePath();
    726                    }
    727                    result = area;
    728
    729                }
    730                else {
    731                    GeneralPath area = new GeneralPath();
    732                    if (orientation == PlotOrientation.HORIZONTAL) {
    733                        area.moveTo(y0A, x0);
    734                        area.lineTo(y1A, x1);
    735                        area.lineTo(y1B, x1);
    736                        area.lineTo(y0B, x0);
    737                        area.closePath();
    738                    }
    739                    else if (orientation == PlotOrientation.VERTICAL) {
    740                        area.moveTo(x0, y0A);
    741                        area.lineTo(x1, y1A);
    742                        area.lineTo(x1, y1B);
    743                        area.lineTo(x0, y0B);
    744                        area.closePath();
    745                    }
    746                    result = area;
    747                }
    748
    749            }
    750
    751            return result;
    752
    753        }
    754
    755        /**
    756         * Returns the negative area for a cross-over section.
    757         *
    758         * @param x0  x coordinate.
    759         * @param y0A  y coordinate A.
    760         * @param y0B  y coordinate B.
    761         * @param x1  x coordinate.
    762         * @param y1A  y coordinate A.
    763         * @param y1B  y coordinate B.
    764         * @param orientation  the plot orientation.
    765         *
    766         * @return The negative area.
    767         */
    768        protected Shape getNegativeArea(float x0, float y0A, float y0B,
    769                                        float x1, float y1A, float y1B,
    770                                        PlotOrientation orientation) {
    771
    772            Shape result = null;
    773
    774            boolean startsNegative = (y0A >= y0B);
    775            boolean endsNegative = (y1A >= y1B);
    776            if (orientation == PlotOrientation.HORIZONTAL) {
    777                startsNegative = (y0B >= y0A);
    778                endsNegative = (y1B >= y1A);
    779            }
    780            if (startsNegative) {  // starts negative
    781                if (endsNegative) {  // all negative
    782                    GeneralPath area = new GeneralPath();
    783                    if (orientation == PlotOrientation.HORIZONTAL) {
    784                        area.moveTo(y0A, x0);
    785                        area.lineTo(y1A, x1);
    786                        area.lineTo(y1B, x1);
    787                        area.lineTo(y0B, x0);
    788                        area.closePath();
    789                    }
    790                    else if (orientation == PlotOrientation.VERTICAL) {
    791                        area.moveTo(x0, y0A);
    792                        area.lineTo(x1, y1A);
    793                        area.lineTo(x1, y1B);
    794                        area.lineTo(x0, y0B);
    795                        area.closePath();
    796                    }
    797                    result = area;
    798                }
    799                else {  // changed from negative to positive
    800                    float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B);
    801                    GeneralPath area = new GeneralPath();
    802                    if (orientation == PlotOrientation.HORIZONTAL) {
    803                        area.moveTo(y0A, x0);
    804                        area.lineTo(p[1], p[0]);
    805                        area.lineTo(y0B, x0);
    806                        area.closePath();
    807                    }
    808                    else if (orientation == PlotOrientation.VERTICAL) {
    809                        area.moveTo(x0, y0A);
    810                        area.lineTo(p[0], p[1]);
    811                        area.lineTo(x0, y0B);
    812                        area.closePath();
    813                    }
    814                    result = area;
    815                }
    816            }
    817            else {
    818                if (endsNegative) {
    819                    // changed from positive to negative
    820                    float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B);
    821                    GeneralPath area = new GeneralPath();
    822                    if (orientation == PlotOrientation.HORIZONTAL) {
    823                        area.moveTo(y1A, x1);
    824                        area.lineTo(p[1], p[0]);
    825                        area.lineTo(y1B, x1);
    826                        area.closePath();
    827                    }
    828                    else if (orientation == PlotOrientation.VERTICAL) {
    829                        area.moveTo(x1, y1A);
    830                        area.lineTo(p[0], p[1]);
    831                        area.lineTo(x1, y1B);
    832                        area.closePath();
    833                    }
    834                    result = area;
    835                }
    836                else {
    837                    // all negative - return null
    838                }
    839
    840            }
    841
    842            return result;
    843
    844        }
    845
    846        /**
    847         * Returns the intersection point of two lines.
    848         *
    849         * @param x1  x1
    850         * @param y1  y1
    851         * @param x2  x2
    852         * @param y2  y2
    853         * @param x3  x3
    854         * @param y3  y3
    855         * @param x4  x4
    856         * @param y4  y4
    857         *
    858         * @return The intersection point.
    859         */
    860        private float[] getIntersection(float x1, float y1, float x2, float y2,
    861                                        float x3, float y3, float x4, float y4) {
    862
    863            float n = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
    864            float d = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
    865            float u = n / d;
    866
    867            float[] result = new float[2];
    868            result[0] = x1 + u * (x2 - x1);
    869            result[1] = y1 + u * (y2 - y1);
    870            return result;
    871
    872        }
    873
    874        /**
    875         * Returns a default legend item for the specified series.  Subclasses
    876         * should override this method to generate customised items.
    877         *
    878         * @param datasetIndex  the dataset index (zero-based).
    879         * @param series  the series index (zero-based).
    880         *
    881         * @return A legend item for the series.
    882         */
    883        public LegendItem getLegendItem(int datasetIndex, int series) {
    884            LegendItem result = null;
    885            XYPlot p = getPlot();
    886            if (p != null) {
    887                XYDataset dataset = p.getDataset(datasetIndex);
    888                if (dataset != null) {
    889                    if (getItemVisible(series, 0)) {
    890                        String label = getLegendItemLabelGenerator().generateLabel(
    891                                dataset, series);
    892                        String description = label;
    893                        String toolTipText = null;
    894                        if (getLegendItemToolTipGenerator() != null) {
    895                            toolTipText
    896                                = getLegendItemToolTipGenerator().generateLabel(
    897                                        dataset, series);
    898                        }
    899                        String urlText = null;
    900                        if (getLegendItemURLGenerator() != null) {
    901                            urlText = getLegendItemURLGenerator().generateLabel(
    902                                    dataset, series);
    903                        }
    904                        Paint paint = getSeriesPaint(series);
    905                        Stroke stroke = getSeriesStroke(series);
    906                        // TODO:  the following hard-coded line needs generalising
    907                        Line2D line = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
    908                        result = new LegendItem(label, description,
    909                                toolTipText, urlText, line, stroke, paint);
    910                    }
    911                }
    912
    913            }
    914
    915            return result;
    916
    917        }
    918
    919        /**
    920         * Tests this renderer for equality with an arbitrary object.
    921         *
    922         * @param obj  the object (<code>null</code> permitted).
    923         *
    924         * @return A boolean.
    925         */
    926        public boolean equals(Object obj) {
    927            if (obj == this) {
    928                return true;
    929            }
    930            if (!(obj instanceof XYDifferenceRenderer)) {
    931                return false;
    932            }
    933            if (!super.equals(obj)) {
    934                return false;
    935            }
    936            XYDifferenceRenderer that = (XYDifferenceRenderer) obj;
    937            if (!PaintUtilities.equal(this.positivePaint, that.positivePaint)) {
    938                return false;
    939            }
    940            if (!PaintUtilities.equal(this.negativePaint, that.negativePaint)) {
    941                return false;
    942            }
    943            if (this.shapesVisible != that.shapesVisible) {
    944                return false;
    945            }
    946            if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) {
    947                return false;
    948            }
    949            if (this.roundXCoordinates != that.roundXCoordinates) {
    950                return false;
    951            }
    952            return true;
    953        }
    954
    955        /**
    956         * Returns a clone of the renderer.
    957         *
    958         * @return A clone.
    959         *
    960         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
    961         */
    962        public Object clone() throws CloneNotSupportedException {
    963            XYDifferenceRenderer clone = (XYDifferenceRenderer) super.clone();
    964            clone.legendLine = ShapeUtilities.clone(this.legendLine);
    965            return clone;
    966        }
    967
    968        /**
    969         * Provides serialization support.
    970         *
    971         * @param stream  the output stream.
    972         *
    973         * @throws IOException  if there is an I/O error.
    974         */
    975        private void writeObject(ObjectOutputStream stream) throws IOException {
    976            stream.defaultWriteObject();
    977            SerialUtilities.writePaint(this.positivePaint, stream);
    978            SerialUtilities.writePaint(this.negativePaint, stream);
    979            SerialUtilities.writeShape(this.legendLine, stream);
    980        }
    981
    982        /**
    983         * Provides serialization support.
    984         *
    985         * @param stream  the input stream.
    986         *
    987         * @throws IOException  if there is an I/O error.
    988         * @throws ClassNotFoundException  if there is a classpath problem.
    989         */
    990        private void readObject(ObjectInputStream stream)
    991            throws IOException, ClassNotFoundException {
    992            stream.defaultReadObject();
    993            this.positivePaint = SerialUtilities.readPaint(stream);
    994            this.negativePaint = SerialUtilities.readPaint(stream);
    995            this.legendLine = SerialUtilities.readShape(stream);
    996        }
    997
    998    }