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    }