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