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