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 and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Richard West, Advanced Micro Devices, Inc. (major rewrite 
034     *                   of difference drawing algorithm);
035     *
036     * Changes:
037     * --------
038     * 30-Apr-2003 : Version 1 (DG);
039     * 30-Jul-2003 : Modified entity constructor (CZ);
040     * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
041     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
042     * 09-Feb-2004 : Updated to support horizontal plot orientation (DG);
043     * 10-Feb-2004 : Added default constructor, setter methods and updated 
044     *               Javadocs (DG);
045     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
046     * 30-Mar-2004 : Fixed bug in getNegativePaint() method (DG);
047     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
048     *               getYValue() (DG);
049     * 25-Aug-2004 : Fixed a bug preventing the use of crosshairs (DG);
050     * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
051     * 19-Jan-2005 : Now accesses only primitive values from dataset (DG);
052     * 22-Feb-2005 : Override getLegendItem(int, int) to return "line" items (DG);
053     * 13-Apr-2005 : Fixed shape positioning bug (id = 1182062) (DG);
054     * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG);
055     * 04-May-2005 : Override equals() method, renamed get/setPlotShapes() -->
056     *               get/setShapesVisible (DG);
057     * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG);
058     * 16-Jun-2005 : Fix bug (1221021) affecting stroke used for each series (DG);
059     * ------------- JFREECHART 1.0.x ---------------------------------------------
060     * 24-Jan-2007 : Added flag to allow rounding of x-coordinates, and fixed
061     *               bug in clone() (DG);
062     * 05-Feb-2007 : Added an extra call to updateCrosshairValues() in 
063     *               drawItemPass1(), to fix bug 1564967 (DG);
064     * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
065     * 08-Mar-2007 : Fixed entity generation (DG);
066     * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
067     * 23-Apr-2007 : Rewrite of difference drawing algorithm to allow use of 
068     *               series with disjoint x-values (RW);
069     * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG);
070     * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG);
071     * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG);
072     * 05-Nov-2007 : Draw item labels if visible (RW);
073     * 
074     */
075    
076    package org.jfree.chart.renderer.xy;
077    
078    import java.awt.Color;
079    import java.awt.Graphics2D;
080    import java.awt.Paint;
081    import java.awt.Shape;
082    import java.awt.Stroke;
083    import java.awt.geom.GeneralPath;
084    import java.awt.geom.Line2D;
085    import java.awt.geom.Rectangle2D;
086    import java.io.IOException;
087    import java.io.ObjectInputStream;
088    import java.io.ObjectOutputStream;
089    import java.io.Serializable;
090    import java.util.Collections;
091    import java.util.LinkedList;
092    
093    import org.jfree.chart.LegendItem;
094    import org.jfree.chart.axis.ValueAxis;
095    import org.jfree.chart.entity.EntityCollection;
096    import org.jfree.chart.entity.XYItemEntity;
097    import org.jfree.chart.event.RendererChangeEvent;
098    import org.jfree.chart.labels.XYToolTipGenerator;
099    import org.jfree.chart.plot.CrosshairState;
100    import org.jfree.chart.plot.PlotOrientation;
101    import org.jfree.chart.plot.PlotRenderingInfo;
102    import org.jfree.chart.plot.XYPlot;
103    import org.jfree.chart.urls.XYURLGenerator;
104    import org.jfree.data.xy.XYDataset;
105    import org.jfree.io.SerialUtilities;
106    import org.jfree.ui.RectangleEdge;
107    import org.jfree.util.PaintUtilities;
108    import org.jfree.util.PublicCloneable;
109    import org.jfree.util.ShapeUtilities;
110    
111    /**
112     * A renderer for an {@link XYPlot} that highlights the differences between two
113     * series.
114     */
115    public class XYDifferenceRenderer extends AbstractXYItemRenderer 
116                                      implements XYItemRenderer, 
117                                                 Cloneable,
118                                                 PublicCloneable,
119                                                 Serializable {
120    
121        /** For serialization. */
122        private static final long serialVersionUID = -8447915602375584857L;
123        
124        /** The paint used to highlight positive differences (y(0) > y(1)). */
125        private transient Paint positivePaint;
126    
127        /** The paint used to highlight negative differences (y(0) < y(1)). */
128        private transient Paint negativePaint;
129    
130        /** Display shapes at each point? */
131        private boolean shapesVisible;
132        
133        /** The shape to display in the legend item. */
134        private transient Shape legendLine;
135    
136        /**
137         * This flag controls whether or not the x-coordinates (in Java2D space) 
138         * are rounded to integers.  When set to true, this can avoid the vertical
139         * striping that anti-aliasing can generate.  However, the rounding may not
140         * be appropriate for output in high resolution formats (for example, 
141         * vector graphics formats such as SVG and PDF).
142         * 
143         * @since 1.0.4
144         */
145        private boolean roundXCoordinates;
146    
147        /**
148         * Creates a new renderer with default attributes.
149         */
150        public XYDifferenceRenderer() {
151            this(Color.green, Color.red, false);
152        }
153        
154        /**
155         * Creates a new renderer.
156         *
157         * @param positivePaint  the highlight color for positive differences 
158         *                       (<code>null</code> not permitted).
159         * @param negativePaint  the highlight color for negative differences 
160         *                       (<code>null</code> not permitted).
161         * @param shapes  draw shapes?
162         */
163        public XYDifferenceRenderer(Paint positivePaint, Paint negativePaint, 
164                                    boolean shapes) {
165            if (positivePaint == null) {
166                throw new IllegalArgumentException(
167                        "Null 'positivePaint' argument.");
168            }
169            if (negativePaint == null) {
170                throw new IllegalArgumentException(
171                        "Null 'negativePaint' argument.");
172            }
173            this.positivePaint = positivePaint;
174            this.negativePaint = negativePaint;
175            this.shapesVisible = shapes;
176            this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
177            this.roundXCoordinates = false;
178        }
179    
180        /**
181         * Returns the paint used to highlight positive differences.
182         *
183         * @return The paint (never <code>null</code>).
184         * 
185         * @see #setPositivePaint(Paint)
186         */
187        public Paint getPositivePaint() {
188            return this.positivePaint;
189        }
190    
191        /**
192         * Sets the paint used to highlight positive differences and sends a
193         * {@link RendererChangeEvent} to all registered listeners.
194         * 
195         * @param paint  the paint (<code>null</code> not permitted).
196         * 
197         * @see #getPositivePaint()
198         */
199        public void setPositivePaint(Paint paint) {
200            if (paint == null) {
201                throw new IllegalArgumentException("Null 'paint' argument.");
202            }
203            this.positivePaint = paint;
204            fireChangeEvent();
205        }
206    
207        /**
208         * Returns the paint used to highlight negative differences.
209         *
210         * @return The paint (never <code>null</code>).
211         * 
212         * @see #setNegativePaint(Paint)
213         */
214        public Paint getNegativePaint() {
215            return this.negativePaint;
216        }
217        
218        /**
219         * Sets the paint used to highlight negative differences.
220         * 
221         * @param paint  the paint (<code>null</code> not permitted).
222         * 
223         * @see #getNegativePaint()
224         */
225        public void setNegativePaint(Paint paint) {
226            if (paint == null) {
227                throw new IllegalArgumentException("Null 'paint' argument.");
228            }
229            this.negativePaint = paint;
230            notifyListeners(new RendererChangeEvent(this));
231        }
232    
233        /**
234         * Returns a flag that controls whether or not shapes are drawn for each 
235         * data value.
236         * 
237         * @return A boolean.
238         * 
239         * @see #setShapesVisible(boolean)
240         */
241        public boolean getShapesVisible() {
242            return this.shapesVisible;
243        }
244    
245        /**
246         * Sets a flag that controls whether or not shapes are drawn for each 
247         * data value, and sends a {@link RendererChangeEvent} to all registered
248         * listeners.
249         * 
250         * @param flag  the flag.
251         * 
252         * @see #getShapesVisible()
253         */
254        public void setShapesVisible(boolean flag) {
255            this.shapesVisible = flag;
256            fireChangeEvent();
257        }
258        
259        /**
260         * Returns the shape used to represent a line in the legend.
261         * 
262         * @return The legend line (never <code>null</code>).
263         * 
264         * @see #setLegendLine(Shape)
265         */
266        public Shape getLegendLine() {
267            return this.legendLine;   
268        }
269        
270        /**
271         * Sets the shape used as a line in each legend item and sends a 
272         * {@link RendererChangeEvent} to all registered listeners.
273         * 
274         * @param line  the line (<code>null</code> not permitted).
275         * 
276         * @see #getLegendLine()
277         */
278        public void setLegendLine(Shape line) {
279            if (line == null) {
280                throw new IllegalArgumentException("Null 'line' argument.");   
281            }
282            this.legendLine = line;
283            fireChangeEvent();
284        }
285    
286        /**
287         * Returns the flag that controls whether or not the x-coordinates (in
288         * Java2D space) are rounded to integer values.
289         * 
290         * @return The flag.
291         * 
292         * @since 1.0.4
293         * 
294         * @see #setRoundXCoordinates(boolean)
295         */
296        public boolean getRoundXCoordinates() {
297            return this.roundXCoordinates;
298        }
299        
300        /**
301         * Sets the flag that controls whether or not the x-coordinates (in 
302         * Java2D space) are rounded to integer values, and sends a 
303         * {@link RendererChangeEvent} to all registered listeners.
304         * 
305         * @param round  the new flag value.
306         * 
307         * @since 1.0.4
308         * 
309         * @see #getRoundXCoordinates()
310         */
311        public void setRoundXCoordinates(boolean round) {
312            this.roundXCoordinates = round;
313            fireChangeEvent();
314        }
315    
316        /**
317         * Initialises the renderer and returns a state object that should be 
318         * passed to subsequent calls to the drawItem() method.  This method will 
319         * be called before the first item is rendered, giving the renderer an 
320         * opportunity to initialise any state information it wants to maintain.  
321         * The renderer can do nothing if it chooses.
322         *
323         * @param g2  the graphics device.
324         * @param dataArea  the area inside the axes.
325         * @param plot  the plot.
326         * @param data  the data.
327         * @param info  an optional info collection object to return data back to 
328         *              the caller.
329         *
330         * @return A state object.
331         */
332        public XYItemRendererState initialise(Graphics2D g2,
333                                              Rectangle2D dataArea,
334                                              XYPlot plot,
335                                              XYDataset data,
336                                              PlotRenderingInfo info) {
337    
338            XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 
339                    info);
340            state.setProcessVisibleItemsOnly(false);
341            return state;
342    
343        }
344    
345        /**
346         * Returns <code>2</code>, the number of passes required by the renderer.  
347         * The {@link XYPlot} will run through the dataset this number of times.
348         * 
349         * @return The number of passes required by the renderer.
350         */
351        public int getPassCount() {
352            return 2;
353        }
354        
355        /**
356         * Draws the visual representation of a single data item.
357         *
358         * @param g2  the graphics device.
359         * @param state  the renderer state.
360         * @param dataArea  the area within which the data is being drawn.
361         * @param info  collects information about the drawing.
362         * @param plot  the plot (can be used to obtain standard color 
363         *              information etc).
364         * @param domainAxis  the domain (horizontal) axis.
365         * @param rangeAxis  the range (vertical) axis.
366         * @param dataset  the dataset.
367         * @param series  the series index (zero-based).
368         * @param item  the item index (zero-based).
369         * @param crosshairState  crosshair information for the plot 
370         *                        (<code>null</code> permitted).
371         * @param pass  the pass index.
372         */
373        public void drawItem(Graphics2D g2,
374                             XYItemRendererState state,
375                             Rectangle2D dataArea,
376                             PlotRenderingInfo info,
377                             XYPlot plot,
378                             ValueAxis domainAxis,
379                             ValueAxis rangeAxis,
380                             XYDataset dataset,
381                             int series,
382                             int item,
383                             CrosshairState crosshairState,
384                             int pass) {
385    
386            if (pass == 0) {
387                drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, 
388                        dataset, series, item, crosshairState);
389            }
390            else if (pass == 1) {
391                drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, 
392                        dataset, series, item, crosshairState);
393            }
394    
395        }
396    
397        /**
398         * Draws the visual representation of a single data item, first pass.
399         *
400         * @param x_graphics  the graphics device.
401         * @param x_dataArea  the area within which the data is being drawn.
402         * @param x_info  collects information about the drawing.
403         * @param x_plot  the plot (can be used to obtain standard color 
404         *                information etc).
405         * @param x_domainAxis  the domain (horizontal) axis.
406         * @param x_rangeAxis  the range (vertical) axis.
407         * @param x_dataset  the dataset.
408         * @param x_series  the series index (zero-based).
409         * @param x_item  the item index (zero-based).
410         * @param x_crosshairState  crosshair information for the plot 
411         *                          (<code>null</code> permitted).
412         */
413        protected void drawItemPass0(Graphics2D x_graphics,
414                                     Rectangle2D x_dataArea,
415                                     PlotRenderingInfo x_info,
416                                     XYPlot x_plot,
417                                     ValueAxis x_domainAxis,
418                                     ValueAxis x_rangeAxis,
419                                     XYDataset x_dataset,
420                                     int x_series,
421                                     int x_item,
422                                     CrosshairState x_crosshairState) {
423    
424            if (!((0 == x_series) && (0 == x_item))) {
425                return;
426            }
427    
428            boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount());
429    
430            // check if either series is a degenerate case (i.e. less than 2 points)
431            if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend)) {
432                return;
433            }
434    
435            // check if series are disjoint (i.e. domain-spans do not overlap)
436            if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset)) {
437                return;
438            }
439    
440            // polygon definitions
441            LinkedList l_minuendXs    = new LinkedList();
442            LinkedList l_minuendYs    = new LinkedList();
443            LinkedList l_subtrahendXs = new LinkedList();
444            LinkedList l_subtrahendYs = new LinkedList();
445            LinkedList l_polygonXs    = new LinkedList();
446            LinkedList l_polygonYs    = new LinkedList();
447    
448            // state
449            int l_minuendItem      = 0;
450            int l_minuendItemCount = x_dataset.getItemCount(0);
451            Double l_minuendCurX   = null;
452            Double l_minuendNextX  = null;
453            Double l_minuendCurY   = null;
454            Double l_minuendNextY  = null;
455            double l_minuendMaxY   = Double.NEGATIVE_INFINITY;
456            double l_minuendMinY   = Double.POSITIVE_INFINITY;
457    
458            int l_subtrahendItem      = 0;
459            int l_subtrahendItemCount = 0; // actual value set below
460            Double l_subtrahendCurX   = null;
461            Double l_subtrahendNextX  = null;
462            Double l_subtrahendCurY   = null;
463            Double l_subtrahendNextY  = null;
464            double l_subtrahendMaxY   = Double.NEGATIVE_INFINITY;
465            double l_subtrahendMinY   = Double.POSITIVE_INFINITY;
466    
467            // if a subtrahend is not specified, assume it is zero
468            if (b_impliedZeroSubtrahend) {
469                l_subtrahendItem      = 0;
470                l_subtrahendItemCount = 2;
471                l_subtrahendCurX      = new Double(x_dataset.getXValue(0, 0));
472                l_subtrahendNextX     = new Double(x_dataset.getXValue(0, 
473                        (l_minuendItemCount - 1)));
474                l_subtrahendCurY      = new Double(0.0);
475                l_subtrahendNextY     = new Double(0.0);
476                l_subtrahendMaxY      = 0.0;
477                l_subtrahendMinY      = 0.0;
478    
479                l_subtrahendXs.add(l_subtrahendCurX);
480                l_subtrahendYs.add(l_subtrahendCurY);
481            }
482            else {
483                l_subtrahendItemCount = x_dataset.getItemCount(1);
484            }
485    
486            boolean b_minuendDone           = false;
487            boolean b_minuendAdvanced       = true;
488            boolean b_minuendAtIntersect    = false;
489            boolean b_minuendFastForward    = false;
490            boolean b_subtrahendDone        = false;
491            boolean b_subtrahendAdvanced    = true;
492            boolean b_subtrahendAtIntersect = false;
493            boolean b_subtrahendFastForward = false;
494            boolean b_colinear              = false;
495    
496            boolean b_positive;
497    
498            // coordinate pairs
499            double l_x1 = 0.0, l_y1 = 0.0; // current minuend point
500            double l_x2 = 0.0, l_y2 = 0.0; // next minuend point
501            double l_x3 = 0.0, l_y3 = 0.0; // current subtrahend point
502            double l_x4 = 0.0, l_y4 = 0.0; // next subtrahend point
503    
504            // fast-forward through leading tails
505            boolean b_fastForwardDone = false;
506            while (!b_fastForwardDone) {
507                // get the x and y coordinates
508                l_x1 = x_dataset.getXValue(0, l_minuendItem);
509                l_y1 = x_dataset.getYValue(0, l_minuendItem);
510                l_x2 = x_dataset.getXValue(0, l_minuendItem + 1);
511                l_y2 = x_dataset.getYValue(0, l_minuendItem + 1);
512    
513                l_minuendCurX  = new Double(l_x1);
514                l_minuendCurY  = new Double(l_y1);
515                l_minuendNextX = new Double(l_x2);
516                l_minuendNextY = new Double(l_y2);
517    
518                if (b_impliedZeroSubtrahend) {
519                    l_x3 = l_subtrahendCurX.doubleValue();
520                    l_y3 = l_subtrahendCurY.doubleValue();
521                    l_x4 = l_subtrahendNextX.doubleValue();
522                    l_y4 = l_subtrahendNextY.doubleValue();
523                }
524                else {
525                    l_x3 = x_dataset.getXValue(1, l_subtrahendItem);
526                    l_y3 = x_dataset.getYValue(1, l_subtrahendItem);
527                    l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1);
528                    l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1);
529    
530                    l_subtrahendCurX  = new Double(l_x3);
531                    l_subtrahendCurY  = new Double(l_y3);
532                    l_subtrahendNextX = new Double(l_x4);
533                    l_subtrahendNextY = new Double(l_y4);
534                }
535    
536                if (l_x2 <= l_x3) {
537                    // minuend needs to be fast forwarded
538                    l_minuendItem++;
539                    b_minuendFastForward = true;
540                    continue;
541                }
542    
543                if (l_x4 <= l_x1) {
544                    // subtrahend needs to be fast forwarded
545                    l_subtrahendItem++;
546                    b_subtrahendFastForward = true;
547                    continue;
548                }
549    
550                // check if initial polygon needs to be clipped
551                if ((l_x3 < l_x1) && (l_x1 < l_x4)) {
552                    // project onto subtrahend
553                    double l_slope   = (l_y4 - l_y3) / (l_x4 - l_x3);
554                    l_subtrahendCurX = l_minuendCurX;
555                    l_subtrahendCurY = new Double((l_slope * l_x1) 
556                            + (l_y3 - (l_slope * l_x3)));
557    
558                    l_subtrahendXs.add(l_subtrahendCurX);
559                    l_subtrahendYs.add(l_subtrahendCurY);
560                }
561    
562                if ((l_x1 < l_x3) && (l_x3 < l_x2)) {
563                    // project onto minuend
564                    double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1);
565                    l_minuendCurX  = l_subtrahendCurX;
566                    l_minuendCurY  = new Double((l_slope * l_x3) 
567                            + (l_y1 - (l_slope * l_x1)));
568    
569                    l_minuendXs.add(l_minuendCurX);
570                    l_minuendYs.add(l_minuendCurY);
571                }
572    
573                l_minuendMaxY    = l_minuendCurY.doubleValue();
574                l_minuendMinY    = l_minuendCurY.doubleValue();
575                l_subtrahendMaxY = l_subtrahendCurY.doubleValue();
576                l_subtrahendMinY = l_subtrahendCurY.doubleValue();
577    
578                b_fastForwardDone = true;
579            }
580    
581            // start of algorithm
582            while (!b_minuendDone && !b_subtrahendDone) {
583                if (!b_minuendDone && !b_minuendFastForward && b_minuendAdvanced) {
584                    l_x1 = x_dataset.getXValue(0, l_minuendItem);
585                    l_y1 = x_dataset.getYValue(0, l_minuendItem);
586                    l_minuendCurX = new Double(l_x1);
587                    l_minuendCurY = new Double(l_y1);
588    
589                    if (!b_minuendAtIntersect) {
590                        l_minuendXs.add(l_minuendCurX);
591                        l_minuendYs.add(l_minuendCurY);
592                    }
593    
594                    l_minuendMaxY = Math.max(l_minuendMaxY, l_y1);
595                    l_minuendMinY = Math.min(l_minuendMinY, l_y1);
596    
597                    l_x2 = x_dataset.getXValue(0, l_minuendItem + 1);
598                    l_y2 = x_dataset.getYValue(0, l_minuendItem + 1);
599                    l_minuendNextX = new Double(l_x2);
600                    l_minuendNextY = new Double(l_y2);
601                }
602    
603                // never updated the subtrahend if it is implied to be zero
604                if (!b_impliedZeroSubtrahend && !b_subtrahendDone 
605                        && !b_subtrahendFastForward && b_subtrahendAdvanced) {
606                    l_x3 = x_dataset.getXValue(1, l_subtrahendItem);
607                    l_y3 = x_dataset.getYValue(1, l_subtrahendItem);
608                    l_subtrahendCurX = new Double(l_x3);
609                    l_subtrahendCurY = new Double(l_y3);
610    
611                    if (!b_subtrahendAtIntersect) {
612                        l_subtrahendXs.add(l_subtrahendCurX);
613                        l_subtrahendYs.add(l_subtrahendCurY);
614                    }
615    
616                    l_subtrahendMaxY = Math.max(l_subtrahendMaxY, l_y3);
617                    l_subtrahendMinY = Math.min(l_subtrahendMinY, l_y3);
618    
619                    l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1);
620                    l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1);
621                    l_subtrahendNextX = new Double(l_x4);
622                    l_subtrahendNextY = new Double(l_y4);
623                }
624    
625                // deassert b_*FastForward (only matters for 1st time through loop)
626                b_minuendFastForward    = false;
627                b_subtrahendFastForward = false;
628    
629                Double l_intersectX = null;
630                Double l_intersectY = null;
631                boolean b_intersect = false;
632    
633                b_minuendAtIntersect    = false;
634                b_subtrahendAtIntersect = false;
635    
636                // check for intersect
637                if ((l_x2 == l_x4) && (l_y2 == l_y4)) {
638                    // check if line segments are colinear
639                    if ((l_x1 == l_x3) && (l_y1 == l_y3)) {
640                        b_colinear = true;
641                    }
642                    else {
643                        // the intersect is at the next point for both the minuend 
644                        // and subtrahend
645                        l_intersectX = new Double(l_x2);
646                        l_intersectY = new Double(l_y2);
647    
648                        b_intersect             = true;
649                        b_minuendAtIntersect    = true;
650                        b_subtrahendAtIntersect = true;
651                     }
652                }
653                else {
654                    // compute common denominator
655                    double l_denominator = ((l_y4 - l_y3) * (l_x2 - l_x1)) 
656                            - ((l_x4 - l_x3) * (l_y2 - l_y1));
657    
658                    // compute common deltas
659                    double l_deltaY = l_y1 - l_y3;
660                    double l_deltaX = l_x1 - l_x3;
661    
662                    // compute numerators
663                    double l_numeratorA = ((l_x4 - l_x3) * l_deltaY) 
664                            - ((l_y4 - l_y3) * l_deltaX);
665                    double l_numeratorB = ((l_x2 - l_x1) * l_deltaY) 
666                            - ((l_y2 - l_y1) * l_deltaX);
667    
668                    // check if line segments are colinear
669                    if ((0 == l_numeratorA) && (0 == l_numeratorB) 
670                            && (0 == l_denominator)) {
671                        b_colinear = true;
672                    }
673                    else {
674                        // check if previously colinear
675                        if (b_colinear) {
676                            // clear colinear points and flag
677                            l_minuendXs.clear();
678                            l_minuendYs.clear();
679                            l_subtrahendXs.clear();
680                            l_subtrahendYs.clear();
681                            l_polygonXs.clear();
682                            l_polygonYs.clear();
683    
684                            b_colinear = false;
685    
686                            // set new starting point for the polygon
687                            boolean b_useMinuend = ((l_x3 <= l_x1) 
688                                    && (l_x1 <= l_x4));
689                            l_polygonXs.add(b_useMinuend ? l_minuendCurX 
690                                    : l_subtrahendCurX);
691                            l_polygonYs.add(b_useMinuend ? l_minuendCurY 
692                                    : l_subtrahendCurY);
693                        }
694    
695                        // compute slope components
696                        double l_slopeA = l_numeratorA / l_denominator;
697                        double l_slopeB = l_numeratorB / l_denominator;
698    
699                        // check if the line segments intersect
700                        if ((0 < l_slopeA) && (l_slopeA <= 1) && (0 < l_slopeB) 
701                                && (l_slopeB <= 1)) {
702                            // compute the point of intersection
703                            double l_xi = l_x1 + (l_slopeA * (l_x2 - l_x1));
704                            double l_yi = l_y1 + (l_slopeA * (l_y2 - l_y1));
705    
706                            l_intersectX            = new Double(l_xi);
707                            l_intersectY            = new Double(l_yi);
708                            b_intersect             = true;
709                            b_minuendAtIntersect    = ((l_xi == l_x2) 
710                                    && (l_yi == l_y2));
711                            b_subtrahendAtIntersect = ((l_xi == l_x4) 
712                                    && (l_yi == l_y4));
713    
714                            // advance minuend and subtrahend to intesect
715                            l_minuendCurX    = l_intersectX;
716                            l_minuendCurY    = l_intersectY;
717                            l_subtrahendCurX = l_intersectX;
718                            l_subtrahendCurY = l_intersectY;
719                        }
720                    }
721                }
722    
723                if (b_intersect) {
724                    // create the polygon
725                    // add the minuend's points to polygon
726                    l_polygonXs.addAll(l_minuendXs);
727                    l_polygonYs.addAll(l_minuendYs);
728    
729                    // add intersection point to the polygon
730                    l_polygonXs.add(l_intersectX);
731                    l_polygonYs.add(l_intersectY);
732    
733                    // add the subtrahend's points to the polygon in reverse
734                    Collections.reverse(l_subtrahendXs);
735                    Collections.reverse(l_subtrahendYs);
736                    l_polygonXs.addAll(l_subtrahendXs);
737                    l_polygonYs.addAll(l_subtrahendYs);
738    
739                    // create an actual polygon
740                    b_positive = (l_subtrahendMaxY <= l_minuendMaxY) 
741                            && (l_subtrahendMinY <= l_minuendMinY);
742                    createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, 
743                            x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);
744    
745                    // clear the point vectors
746                    l_minuendXs.clear();
747                    l_minuendYs.clear();
748                    l_subtrahendXs.clear();
749                    l_subtrahendYs.clear();
750                    l_polygonXs.clear();
751                    l_polygonYs.clear();
752    
753                    // set the maxY and minY values to intersect y-value
754                    double l_y       = l_intersectY.doubleValue();
755                    l_minuendMaxY    = l_y;
756                    l_subtrahendMaxY = l_y;
757                    l_minuendMinY    = l_y;
758                    l_subtrahendMinY = l_y;
759    
760                    // add interection point to new polygon
761                    l_polygonXs.add(l_intersectX);
762                    l_polygonYs.add(l_intersectY);
763                }
764    
765                // advance the minuend if needed
766                if (l_x2 <= l_x4) {
767                    l_minuendItem++;
768                    b_minuendAdvanced = true;
769                }
770                else {
771                    b_minuendAdvanced = false;
772                }
773    
774                // advance the subtrahend if needed
775                if (l_x4 <= l_x2) {
776                    l_subtrahendItem++;
777                    b_subtrahendAdvanced = true;
778                }
779                else {
780                    b_subtrahendAdvanced = false;
781                }
782    
783                b_minuendDone    = (l_minuendItem == (l_minuendItemCount - 1));
784                b_subtrahendDone = (l_subtrahendItem == (l_subtrahendItemCount 
785                        - 1));
786            }
787    
788            // check if the final polygon needs to be clipped
789            if (b_minuendDone && (l_x3 < l_x2) && (l_x2 < l_x4)) {
790                // project onto subtrahend
791                double l_slope    = (l_y4 - l_y3) / (l_x4 - l_x3);
792                l_subtrahendNextX = l_minuendNextX;
793                l_subtrahendNextY = new Double((l_slope * l_x2) 
794                        + (l_y3 - (l_slope * l_x3)));
795            }
796    
797            if (b_subtrahendDone && (l_x1 < l_x4) && (l_x4 < l_x2)) {
798                // project onto minuend
799                double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1);
800                l_minuendNextX = l_subtrahendNextX;
801                l_minuendNextY = new Double((l_slope * l_x4) 
802                        + (l_y1 - (l_slope * l_x1)));
803            }
804    
805            // consider last point of minuend and subtrahend for determining 
806            // positivity
807            l_minuendMaxY    = Math.max(l_minuendMaxY, 
808                    l_minuendNextY.doubleValue());
809            l_subtrahendMaxY = Math.max(l_subtrahendMaxY, 
810                    l_subtrahendNextY.doubleValue());
811            l_minuendMinY    = Math.min(l_minuendMinY, 
812                    l_minuendNextY.doubleValue());
813            l_subtrahendMinY = Math.min(l_subtrahendMinY, 
814                    l_subtrahendNextY.doubleValue());
815    
816            // add the last point of the minuned and subtrahend
817            l_minuendXs.add(l_minuendNextX);
818            l_minuendYs.add(l_minuendNextY);
819            l_subtrahendXs.add(l_subtrahendNextX);
820            l_subtrahendYs.add(l_subtrahendNextY);
821    
822            // create the polygon
823            // add the minuend's points to polygon
824            l_polygonXs.addAll(l_minuendXs);
825            l_polygonYs.addAll(l_minuendYs);
826    
827            // add the subtrahend's points to the polygon in reverse
828            Collections.reverse(l_subtrahendXs);
829            Collections.reverse(l_subtrahendYs);
830            l_polygonXs.addAll(l_subtrahendXs);
831            l_polygonYs.addAll(l_subtrahendYs);
832    
833            // create an actual polygon
834            b_positive = (l_subtrahendMaxY <= l_minuendMaxY) 
835                    && (l_subtrahendMinY <= l_minuendMinY);
836            createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, 
837                    x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);
838        }
839    
840        /**
841         * Draws the visual representation of a single data item, second pass.  In 
842         * the second pass, the renderer draws the lines and shapes for the 
843         * individual points in the two series.
844         *
845         * @param x_graphics  the graphics device.
846         * @param x_dataArea  the area within which the data is being drawn.
847         * @param x_info  collects information about the drawing.
848         * @param x_plot  the plot (can be used to obtain standard color 
849         *         information etc).
850         * @param x_domainAxis  the domain (horizontal) axis.
851         * @param x_rangeAxis  the range (vertical) axis.
852         * @param x_dataset  the dataset.
853         * @param x_series  the series index (zero-based).
854         * @param x_item  the item index (zero-based).
855         * @param x_crosshairState  crosshair information for the plot 
856         *                          (<code>null</code> permitted).
857         */
858        protected void drawItemPass1(Graphics2D x_graphics,
859                                     Rectangle2D x_dataArea,
860                                     PlotRenderingInfo x_info,
861                                     XYPlot x_plot,
862                                     ValueAxis x_domainAxis,
863                                     ValueAxis x_rangeAxis,
864                                     XYDataset x_dataset,
865                                     int x_series,
866                                     int x_item,
867                                     CrosshairState x_crosshairState) {
868    
869            Shape l_entityArea = null;
870            EntityCollection l_entities = null;
871            if (null != x_info) {
872                l_entities = x_info.getOwner().getEntityCollection();
873            }
874    
875            Paint l_seriesPaint   = getItemPaint(x_series, x_item);
876            Stroke l_seriesStroke = getItemStroke(x_series, x_item);
877            x_graphics.setPaint(l_seriesPaint);
878            x_graphics.setStroke(l_seriesStroke);
879    
880            PlotOrientation l_orientation      = x_plot.getOrientation();
881            RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge();
882            RectangleEdge l_rangeAxisLocation  = x_plot.getRangeAxisEdge();
883    
884            double l_x0 = x_dataset.getXValue(x_series, x_item);
885            double l_y0 = x_dataset.getYValue(x_series, x_item);
886            double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea, 
887                    l_domainAxisLocation);
888            double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea, 
889                    l_rangeAxisLocation);
890    
891            if (getShapesVisible()) {
892                Shape l_shape = getItemShape(x_series, x_item);
893                if (l_orientation == PlotOrientation.HORIZONTAL) {
894                    l_shape = ShapeUtilities.createTranslatedShape(l_shape, 
895                            l_y1, l_x1);
896                }
897                else {
898                    l_shape = ShapeUtilities.createTranslatedShape(l_shape, 
899                            l_x1, l_y1);
900                }
901                if (l_shape.intersects(x_dataArea)) {
902                    x_graphics.setPaint(getItemPaint(x_series, x_item));
903                    x_graphics.fill(l_shape);
904                }
905                l_entityArea = l_shape;
906            }
907    
908            // add an entity for the item...
909            if (null != l_entities) {
910                if (null == l_entityArea) {
911                    l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2), 
912                            4, 4);
913                }
914                String l_tip = null;
915                XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series, 
916                        x_item);
917                if (null != l_tipGenerator) {
918                    l_tip = l_tipGenerator.generateToolTip(x_dataset, x_series, 
919                            x_item);
920                }
921                String l_url = null;
922                XYURLGenerator l_urlGenerator = getURLGenerator();
923                if (null != l_urlGenerator) {
924                    l_url = l_urlGenerator.generateURL(x_dataset, x_series, 
925                            x_item);
926                }
927                XYItemEntity l_entity = new XYItemEntity(l_entityArea, x_dataset, 
928                        x_series, x_item, l_tip, l_url);
929                l_entities.add(l_entity);
930            }
931    
932            // draw the item label if there is one...
933            if (isItemLabelVisible(x_series, x_item)) {
934                drawItemLabel(x_graphics, l_orientation, x_dataset, x_series,
935                              x_item, l_x1, l_y1, (l_y1 < 0.0));
936            }
937    
938            int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis);
939            int l_rangeAxisIndex  = x_plot.getRangeAxisIndex(x_rangeAxis);
940            updateCrosshairValues(x_crosshairState, l_x0, l_y0, l_domainAxisIndex,
941                                  l_rangeAxisIndex, l_x1, l_y1, l_orientation);
942    
943            if (0 == x_item) {
944                return;
945            }
946    
947            double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series, 
948                    (x_item - 1)), x_dataArea, l_domainAxisLocation);
949            double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, 
950                    (x_item - 1)), x_dataArea, l_rangeAxisLocation);
951    
952            Line2D l_line = null;
953            if (PlotOrientation.HORIZONTAL == l_orientation) {
954                l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2);
955            }
956            else if (PlotOrientation.VERTICAL == l_orientation) {
957                l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2);
958            }
959     
960            if ((null != l_line) && l_line.intersects(x_dataArea)) {
961                x_graphics.setPaint(getItemPaint(x_series, x_item));
962                x_graphics.setStroke(getItemStroke(x_series, x_item));
963                x_graphics.draw(l_line);
964            }
965        }
966    
967        /**
968         * Determines if a dataset is degenerate.  A degenerate dataset is a 
969         * dataset where either series has less than two (2) points.
970         *
971         * @param x_dataset  the dataset.
972         * @param x_impliedZeroSubtrahend  if false, do not check the subtrahend
973         *
974         * @return true if the dataset is degenerate.
975         */
976        private boolean isEitherSeriesDegenerate(XYDataset x_dataset, 
977                boolean x_impliedZeroSubtrahend) {
978    
979            if (x_impliedZeroSubtrahend) {
980                return (x_dataset.getItemCount(0) < 2);
981            }
982    
983            return ((x_dataset.getItemCount(0) < 2) 
984                    || (x_dataset.getItemCount(1) < 2));
985        }
986    
987        /**
988         * Determines if the two (2) series are disjoint.
989         * Disjoint series do not overlap in the domain space.
990         *
991         * @param x_dataset  the dataset.
992         *
993         * @return true if the dataset is degenerate.
994         */
995        private boolean areSeriesDisjoint(XYDataset x_dataset) {
996    
997            int l_minuendItemCount = x_dataset.getItemCount(0);
998            double l_minuendFirst  = x_dataset.getXValue(0, 0);
999            double l_minuendLast   = x_dataset.getXValue(0, l_minuendItemCount - 1);
1000    
1001            int l_subtrahendItemCount = x_dataset.getItemCount(1);
1002            double l_subtrahendFirst  = x_dataset.getXValue(1, 0);
1003            double l_subtrahendLast   = x_dataset.getXValue(1, 
1004                    l_subtrahendItemCount - 1);
1005    
1006            return ((l_minuendLast < l_subtrahendFirst) 
1007                    || (l_subtrahendLast < l_minuendFirst));
1008        }
1009    
1010        /**
1011         * Draws the visual representation of a polygon
1012         *
1013         * @param x_graphics  the graphics device.
1014         * @param x_dataArea  the area within which the data is being drawn.
1015         * @param x_plot  the plot (can be used to obtain standard color
1016         *                information etc).
1017         * @param x_domainAxis  the domain (horizontal) axis.
1018         * @param x_rangeAxis  the range (vertical) axis.
1019         * @param x_positive  indicates if the polygon is positive (true) or 
1020         *                    negative (false).
1021         * @param x_xValues  a linked list of the x values (expects values to be 
1022         *                   of type Double).
1023         * @param x_yValues  a linked list of the y values (expects values to be 
1024         *                   of type Double).
1025         */
1026        private void createPolygon (Graphics2D x_graphics,
1027                                    Rectangle2D x_dataArea,
1028                                    XYPlot x_plot,
1029                                    ValueAxis x_domainAxis,
1030                                    ValueAxis x_rangeAxis,
1031                                    boolean x_positive,
1032                                    LinkedList x_xValues,
1033                                    LinkedList x_yValues) {
1034    
1035            PlotOrientation l_orientation      = x_plot.getOrientation();
1036            RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge();
1037            RectangleEdge l_rangeAxisLocation  = x_plot.getRangeAxisEdge();
1038    
1039            Object[] l_xValues = x_xValues.toArray();
1040            Object[] l_yValues = x_yValues.toArray();
1041    
1042            GeneralPath l_path = new GeneralPath();
1043    
1044            if (PlotOrientation.VERTICAL == l_orientation) {
1045                double l_x = x_domainAxis.valueToJava2D((
1046                        (Double) l_xValues[0]).doubleValue(), x_dataArea, 
1047                        l_domainAxisLocation);
1048                if (this.roundXCoordinates) {
1049                    l_x = Math.rint(l_x);
1050                }
1051    
1052                double l_y = x_rangeAxis.valueToJava2D((
1053                        (Double) l_yValues[0]).doubleValue(), x_dataArea, 
1054                        l_rangeAxisLocation);
1055    
1056                l_path.moveTo((float) l_x, (float) l_y);
1057                for (int i = 1; i < l_xValues.length; i++) {
1058                    l_x = x_domainAxis.valueToJava2D((
1059                            (Double) l_xValues[i]).doubleValue(), x_dataArea, 
1060                            l_domainAxisLocation);
1061                    if (this.roundXCoordinates) {
1062                        l_x = Math.rint(l_x);
1063                    }
1064    
1065                    l_y = x_rangeAxis.valueToJava2D((
1066                            (Double) l_yValues[i]).doubleValue(), x_dataArea, 
1067                            l_rangeAxisLocation);
1068                    l_path.lineTo((float) l_x, (float) l_y);
1069                }
1070                l_path.closePath();
1071            }
1072            else {
1073                double l_x = x_domainAxis.valueToJava2D((
1074                        (Double) l_xValues[0]).doubleValue(), x_dataArea, 
1075                        l_domainAxisLocation);
1076                if (this.roundXCoordinates) {
1077                    l_x = Math.rint(l_x);
1078                }
1079    
1080                double l_y = x_rangeAxis.valueToJava2D((
1081                        (Double) l_yValues[0]).doubleValue(), x_dataArea, 
1082                        l_rangeAxisLocation);
1083    
1084                l_path.moveTo((float) l_y, (float) l_x);
1085                for (int i = 1; i < l_xValues.length; i++) {
1086                    l_x = x_domainAxis.valueToJava2D((
1087                            (Double) l_xValues[i]).doubleValue(), x_dataArea, 
1088                            l_domainAxisLocation);
1089                    if (this.roundXCoordinates) {
1090                        l_x = Math.rint(l_x);
1091                    }
1092    
1093                    l_y = x_rangeAxis.valueToJava2D((
1094                            (Double) l_yValues[i]).doubleValue(), x_dataArea, 
1095                            l_rangeAxisLocation);
1096                    l_path.lineTo((float) l_y, (float) l_x);
1097                }
1098                l_path.closePath();
1099            }
1100    
1101            if (l_path.intersects(x_dataArea)) {
1102                x_graphics.setPaint(x_positive ? getPositivePaint() 
1103                        : getNegativePaint());
1104                x_graphics.fill(l_path);
1105            }
1106        }
1107    
1108        /**
1109         * Returns a default legend item for the specified series.  Subclasses 
1110         * should override this method to generate customised items.
1111         *
1112         * @param datasetIndex  the dataset index (zero-based).
1113         * @param series  the series index (zero-based).
1114         *
1115         * @return A legend item for the series.
1116         */
1117        public LegendItem getLegendItem(int datasetIndex, int series) {
1118            LegendItem result = null;
1119            XYPlot p = getPlot();
1120            if (p != null) {
1121                XYDataset dataset = p.getDataset(datasetIndex);
1122                if (dataset != null) {
1123                    if (getItemVisible(series, 0)) {
1124                        String label = getLegendItemLabelGenerator().generateLabel(
1125                                dataset, series);
1126                        String description = label;
1127                        String toolTipText = null;
1128                        if (getLegendItemToolTipGenerator() != null) {
1129                            toolTipText 
1130                                = getLegendItemToolTipGenerator().generateLabel(
1131                                        dataset, series);
1132                        }
1133                        String urlText = null;
1134                        if (getLegendItemURLGenerator() != null) {
1135                            urlText = getLegendItemURLGenerator().generateLabel(
1136                                    dataset, series);
1137                        }
1138                        Paint paint = lookupSeriesPaint(series);
1139                        Stroke stroke = lookupSeriesStroke(series);
1140                        // TODO:  the following hard-coded line needs generalising
1141                        Line2D line = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
1142                        result = new LegendItem(label, description, 
1143                                toolTipText, urlText, line, stroke, paint);
1144                        result.setDataset(dataset);
1145                        result.setDatasetIndex(datasetIndex);
1146                        result.setSeriesKey(dataset.getSeriesKey(series));
1147                        result.setSeriesIndex(series);
1148                    }
1149                }
1150    
1151            }
1152    
1153            return result;
1154    
1155        }
1156    
1157        /**
1158         * Tests this renderer for equality with an arbitrary object.
1159         * 
1160         * @param obj  the object (<code>null</code> permitted).
1161         * 
1162         * @return A boolean.
1163         */    
1164        public boolean equals(Object obj) {
1165            if (obj == this) {
1166                return true;   
1167            }
1168            if (!(obj instanceof XYDifferenceRenderer)) {
1169                return false;   
1170            }
1171            if (!super.equals(obj)) {
1172                return false;   
1173            }
1174            XYDifferenceRenderer that = (XYDifferenceRenderer) obj;
1175            if (!PaintUtilities.equal(this.positivePaint, that.positivePaint)) {
1176                return false;   
1177            }
1178            if (!PaintUtilities.equal(this.negativePaint, that.negativePaint)) {
1179                return false;   
1180            }
1181            if (this.shapesVisible != that.shapesVisible) {
1182                return false;   
1183            }
1184            if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) {
1185                return false;   
1186            }
1187            if (this.roundXCoordinates != that.roundXCoordinates) {
1188                return false;
1189            }
1190            return true;
1191        }
1192        
1193        /**
1194         * Returns a clone of the renderer.
1195         * 
1196         * @return A clone.
1197         * 
1198         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
1199         */
1200        public Object clone() throws CloneNotSupportedException {
1201            XYDifferenceRenderer clone = (XYDifferenceRenderer) super.clone();
1202            clone.legendLine = ShapeUtilities.clone(this.legendLine);
1203            return clone;
1204        }
1205    
1206        /**
1207         * Provides serialization support.
1208         *
1209         * @param stream  the output stream.
1210         *
1211         * @throws IOException  if there is an I/O error.
1212         */
1213        private void writeObject(ObjectOutputStream stream) throws IOException {
1214            stream.defaultWriteObject();
1215            SerialUtilities.writePaint(this.positivePaint, stream);
1216            SerialUtilities.writePaint(this.negativePaint, stream);
1217            SerialUtilities.writeShape(this.legendLine, stream);
1218        }
1219    
1220        /**
1221         * Provides serialization support.
1222         *
1223         * @param stream  the input stream.
1224         *
1225         * @throws IOException  if there is an I/O error.
1226         * @throws ClassNotFoundException  if there is a classpath problem.
1227         */
1228        private void readObject(ObjectInputStream stream) 
1229            throws IOException, ClassNotFoundException {
1230            stream.defaultReadObject();
1231            this.positivePaint = SerialUtilities.readPaint(stream);
1232            this.negativePaint = SerialUtilities.readPaint(stream);
1233            this.legendLine = SerialUtilities.readShape(stream);
1234        }
1235    
1236    }