001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it 
010     * under the terms of the GNU Lesser General Public License as published by 
011     * the Free Software Foundation; either version 2.1 of the License, or 
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but 
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022     * USA.  
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     *
027     * --------------------
028     * FastScatterPlot.java
029     * --------------------
030     * (C) Copyright 2002-2005, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Arnaud Lelievre;
034     *
035     * $Id: FastScatterPlot.java,v 1.11.2.3 2005/10/25 20:52:08 mungady Exp $
036     *
037     * Changes (from 29-Oct-2002)
038     * --------------------------
039     * 29-Oct-2002 : Added standard header (DG);
040     * 07-Nov-2002 : Fixed errors reported by Checkstyle (DG);
041     * 26-Mar-2003 : Implemented Serializable (DG);
042     * 19-Aug-2003 : Implemented Cloneable (DG);
043     * 08-Sep-2003 : Added internationalization via use of properties 
044     *               resourceBundle (RFE 690236) (AL); 
045     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
046     * 12-Nov-2003 : Implemented zooming (DG);
047     * 21-Jan-2004 : Update for renamed method in ValueAxis (DG);
048     * 26-Jan-2004 : Added domain and range grid lines (DG);
049     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
050     * 29-Sep-2004 : Removed hard-coded color (DG);
051     * 04-Oct-2004 : Reworked equals() method and renamed ArrayUtils 
052     *               --> ArrayUtilities (DG);
053     * 12-Nov-2004 : Implemented the new Zoomable interface (DG);
054     * 05-May-2005 : Updated draw() method parameters (DG);
055     * 16-Jun-2005 : Added get/setData() methods (DG);
056     *
057     */
058    
059    package org.jfree.chart.plot;
060    
061    import java.awt.AlphaComposite;
062    import java.awt.BasicStroke;
063    import java.awt.Color;
064    import java.awt.Composite;
065    import java.awt.Graphics2D;
066    import java.awt.Paint;
067    import java.awt.Shape;
068    import java.awt.Stroke;
069    import java.awt.geom.Line2D;
070    import java.awt.geom.Point2D;
071    import java.awt.geom.Rectangle2D;
072    import java.io.IOException;
073    import java.io.ObjectInputStream;
074    import java.io.ObjectOutputStream;
075    import java.io.Serializable;
076    import java.util.Iterator;
077    import java.util.List;
078    import java.util.ResourceBundle;
079    
080    import org.jfree.chart.axis.AxisSpace;
081    import org.jfree.chart.axis.AxisState;
082    import org.jfree.chart.axis.ValueAxis;
083    import org.jfree.chart.axis.ValueTick;
084    import org.jfree.chart.event.PlotChangeEvent;
085    import org.jfree.data.Range;
086    import org.jfree.io.SerialUtilities;
087    import org.jfree.ui.RectangleEdge;
088    import org.jfree.ui.RectangleInsets;
089    import org.jfree.util.ArrayUtilities;
090    import org.jfree.util.ObjectUtilities;
091    import org.jfree.util.PaintUtilities;
092    
093    /**
094     * A fast scatter plot.
095     */
096    public class FastScatterPlot extends Plot implements ValueAxisPlot, 
097                                                         Zoomable, 
098                                                         Cloneable, Serializable {
099    
100        /** For serialization. */
101        private static final long serialVersionUID = 7871545897358563521L;
102        
103        /** The default grid line stroke. */
104        public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke(0.5f,
105                BasicStroke.CAP_BUTT,
106                BasicStroke.JOIN_BEVEL,
107                0.0f,
108                new float[] {2.0f, 2.0f},
109                0.0f);
110    
111        /** The default grid line paint. */
112        public static final Paint DEFAULT_GRIDLINE_PAINT = Color.lightGray;
113    
114        /** The data. */
115        private float[][] data;
116    
117        /** The x data range. */
118        private Range xDataRange;
119    
120        /** The y data range. */
121        private Range yDataRange;
122    
123        /** The domain axis (used for the x-values). */
124        private ValueAxis domainAxis;
125    
126        /** The range axis (used for the y-values). */
127        private ValueAxis rangeAxis;
128    
129        /** The paint used to plot data points. */
130        private transient Paint paint;
131    
132        /** A flag that controls whether the domain grid-lines are visible. */
133        private boolean domainGridlinesVisible;
134    
135        /** The stroke used to draw the domain grid-lines. */
136        private transient Stroke domainGridlineStroke;
137    
138        /** The paint used to draw the domain grid-lines. */
139        private transient Paint domainGridlinePaint;
140    
141        /** A flag that controls whether the range grid-lines are visible. */
142        private boolean rangeGridlinesVisible;
143    
144        /** The stroke used to draw the range grid-lines. */
145        private transient Stroke rangeGridlineStroke;
146    
147        /** The paint used to draw the range grid-lines. */
148        private transient Paint rangeGridlinePaint;
149    
150        /** The resourceBundle for the localization. */
151        protected static ResourceBundle localizationResources = 
152            ResourceBundle.getBundle("org.jfree.chart.plot.LocalizationBundle");
153    
154        /**
155         * Creates an empty plot.
156         */
157        public FastScatterPlot() {
158            this(null, null, null);    
159        }
160        
161        /**
162         * Creates a new fast scatter plot.
163         * <P>
164         * The data is an array of x, y values:  data[0][i] = x, data[1][i] = y.
165         *
166         * @param data  the data.
167         * @param domainAxis  the domain (x) axis.
168         * @param rangeAxis  the range (y) axis.
169         */
170        public FastScatterPlot(float[][] data, 
171                               ValueAxis domainAxis, ValueAxis rangeAxis) {
172    
173            super();
174    
175            this.data = data;
176            this.xDataRange = calculateXDataRange(data);
177            this.yDataRange = calculateYDataRange(data);
178            this.domainAxis = domainAxis;
179            if (domainAxis != null) {
180                domainAxis.setPlot(this);
181                domainAxis.addChangeListener(this);
182            }
183    
184            this.rangeAxis = rangeAxis;
185            if (rangeAxis != null) {
186                rangeAxis.setPlot(this);
187                rangeAxis.addChangeListener(this);
188            }
189    
190            this.paint = Color.red;
191            
192            this.domainGridlinesVisible = true;
193            this.domainGridlinePaint = FastScatterPlot.DEFAULT_GRIDLINE_PAINT;
194            this.domainGridlineStroke = FastScatterPlot.DEFAULT_GRIDLINE_STROKE;
195    
196            this.rangeGridlinesVisible = true;
197            this.rangeGridlinePaint = FastScatterPlot.DEFAULT_GRIDLINE_PAINT;
198            this.rangeGridlineStroke = FastScatterPlot.DEFAULT_GRIDLINE_STROKE;
199        
200        }
201    
202        /**
203         * Returns a short string describing the plot type.
204         *
205         * @return A short string describing the plot type.
206         */
207        public String getPlotType() {
208            return localizationResources.getString("Fast_Scatter_Plot");
209        }
210    
211        /**
212         * Returns the data array used by the plot.
213         * 
214         * @return The data array (possibly <code>null</code>).
215         */
216        public float[][] getData() {
217            return this.data;   
218        }
219        
220        /**
221         * Sets the data array used by the plot and sends a {@link PlotChangeEvent}
222         * to all registered listeners.
223         * 
224         * @param data  the data array (<code>null</code> permitted).
225         */
226        public void setData(float[][] data) {
227            this.data = data;
228            notifyListeners(new PlotChangeEvent(this));
229        }
230        
231        /**
232         * Returns the orientation of the plot.
233         * 
234         * @return The orientation (always {@link PlotOrientation#VERTICAL}).
235         */
236        public PlotOrientation getOrientation() {
237            return PlotOrientation.VERTICAL;    
238        }
239        
240        /**
241         * Returns the domain axis for the plot.  If the domain axis for this plot
242         * is null, then the method will return the parent plot's domain axis (if
243         * there is a parent plot).
244         *
245         * @return The domain axis.
246         */
247        public ValueAxis getDomainAxis() {
248            return this.domainAxis;
249        }
250    
251        /**
252         * Returns the range axis for the plot.  If the range axis for this plot is
253         * null, then the method will return the parent plot's range axis (if
254         * there is a parent plot).
255         *
256         * @return The range axis.
257         */
258        public ValueAxis getRangeAxis() {
259            return this.rangeAxis;
260        }
261    
262        /**
263         * Returns the paint used to plot data points.
264         *
265         * @return The paint.
266         */
267        public Paint getPaint() {
268            return this.paint;
269        }
270    
271        /**
272         * Sets the color for the data points and sends a {@link PlotChangeEvent} 
273         * to all registered listeners.
274         *
275         * @param paint  the paint (<code>null</code> not permitted).
276         */
277        public void setPaint(Paint paint) {
278            if (paint == null) {
279                throw new IllegalArgumentException("Null 'paint' argument.");
280            }
281            this.paint = paint;
282            notifyListeners(new PlotChangeEvent(this));
283        }
284    
285        /**
286         * Returns <code>true</code> if the domain gridlines are visible, and 
287         * <code>false<code> otherwise.
288         *
289         * @return <code>true</code> or <code>false</code>.
290         */
291        public boolean isDomainGridlinesVisible() {
292            return this.domainGridlinesVisible;
293        }
294    
295        /**
296         * Sets the flag that controls whether or not the domain grid-lines are 
297         * visible.  If the flag value is changed, a {@link PlotChangeEvent} is 
298         * sent to all registered listeners.
299         *
300         * @param visible  the new value of the flag.
301         */
302        public void setDomainGridlinesVisible(boolean visible) {
303            if (this.domainGridlinesVisible != visible) {
304                this.domainGridlinesVisible = visible;
305                notifyListeners(new PlotChangeEvent(this));
306            }
307        }
308    
309        /**
310         * Returns the stroke for the grid-lines (if any) plotted against the 
311         * domain axis.
312         *
313         * @return The stroke.
314         */
315        public Stroke getDomainGridlineStroke() {
316            return this.domainGridlineStroke;
317        }
318    
319        /**
320         * Sets the stroke for the grid lines plotted against the domain axis.
321         * <p>
322         * If you set this to <code>null</code>, no grid lines will be drawn.
323         *
324         * @param stroke  the stroke (<code>null</code> permitted).
325         */
326        public void setDomainGridlineStroke(Stroke stroke) {
327            this.domainGridlineStroke = stroke;
328            notifyListeners(new PlotChangeEvent(this));
329        }
330    
331        /**
332         * Returns the paint for the grid lines (if any) plotted against the domain
333         * axis.
334         *
335         * @return The paint.
336         */
337        public Paint getDomainGridlinePaint() {
338            return this.domainGridlinePaint;
339        }
340    
341        /**
342         * Sets the paint for the grid lines plotted against the domain axis.
343         * <p>
344         * If you set this to <code>null</code>, no grid lines will be drawn.
345         *
346         * @param paint  the paint (<code>null</code> permitted).
347         */
348        public void setDomainGridlinePaint(Paint paint) {
349            this.domainGridlinePaint = paint;
350            notifyListeners(new PlotChangeEvent(this));
351        }
352    
353        /**
354         * Returns <code>true</code> if the range axis grid is visible, and 
355         * <code>false<code> otherwise.
356         *
357         * @return <code>true</code> or <code>false</code>.
358         */
359        public boolean isRangeGridlinesVisible() {
360            return this.rangeGridlinesVisible;
361        }
362    
363        /**
364         * Sets the flag that controls whether or not the range axis grid lines are
365         * visible.  If the flag value is changed, a {@link PlotChangeEvent} is 
366         * sent to all registered listeners.
367         *
368         * @param visible  the new value of the flag.
369         */
370        public void setRangeGridlinesVisible(boolean visible) {
371            if (this.rangeGridlinesVisible != visible) {
372                this.rangeGridlinesVisible = visible;
373                notifyListeners(new PlotChangeEvent(this));
374            }
375        }
376    
377        /**
378         * Returns the stroke for the grid lines (if any) plotted against the range
379         * axis.
380         *
381         * @return The stroke.
382         */
383        public Stroke getRangeGridlineStroke() {
384            return this.rangeGridlineStroke;
385        }
386    
387        /**
388         * Sets the stroke for the grid lines plotted against the range axis.
389         * <p>
390         * If you set this to <code>null</code>, no grid lines will be drawn.
391         *
392         * @param stroke  the stroke (<code>null</code> permitted).
393         */
394        public void setRangeGridlineStroke(Stroke stroke) {
395            this.rangeGridlineStroke = stroke;
396            notifyListeners(new PlotChangeEvent(this));
397        }
398    
399        /**
400         * Returns the paint for the grid lines (if any) plotted against the range 
401         * axis.
402         *
403         * @return The paint.
404         */
405        public Paint getRangeGridlinePaint() {
406            return this.rangeGridlinePaint;
407        }
408    
409        /**
410         * Sets the paint for the grid lines plotted against the range axis.
411         * <p>
412         * If you set this to <code>null</code>, no grid lines will be drawn.
413         *
414         * @param paint  the paint (<code>null</code> permitted).
415         */
416        public void setRangeGridlinePaint(Paint paint) {
417            this.rangeGridlinePaint = paint;
418            notifyListeners(new PlotChangeEvent(this));
419        }
420    
421        /**
422         * Draws the fast scatter plot on a Java 2D graphics device (such as the 
423         * screen or a printer).
424         *
425         * @param g2  the graphics device.
426         * @param area   the area within which the plot (including axis labels)
427         *                   should be drawn.
428         * @param anchor  the anchor point (<code>null</code> permitted).
429         * @param parentState  the state from the parent plot, if there is one.
430         * @param info  collects chart drawing information (<code>null</code> 
431         *              permitted).
432         */
433        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
434                         PlotState parentState,
435                         PlotRenderingInfo info) {
436    
437            // set up info collection...
438            if (info != null) {
439                info.setPlotArea(area);
440            }
441    
442            // adjust the drawing area for plot insets (if any)...
443            RectangleInsets insets = getInsets();
444            insets.trim(area);
445    
446            AxisSpace space = new AxisSpace();
447            space = this.domainAxis.reserveSpace(
448                g2, this, area, RectangleEdge.BOTTOM, space
449            );
450            space = this.rangeAxis.reserveSpace(
451                g2, this, area, RectangleEdge.LEFT, space
452            );
453            Rectangle2D dataArea = space.shrink(area, null);
454    
455            if (info != null) {
456                info.setDataArea(dataArea);
457            }
458    
459            // draw the plot background and axes...
460            drawBackground(g2, dataArea);
461    
462            AxisState domainAxisState = null;
463            AxisState rangeAxisState = null;
464            if (this.domainAxis != null) {
465                domainAxisState = this.domainAxis.draw(
466                    g2, dataArea.getMaxY(), area, dataArea, 
467                    RectangleEdge.BOTTOM, info
468                );
469            }
470            if (this.rangeAxis != null) {
471                rangeAxisState = this.rangeAxis.draw(
472                    g2, dataArea.getMinX(), area, dataArea, 
473                    RectangleEdge.LEFT, info
474                );
475            }
476            drawDomainGridlines(g2, dataArea, domainAxisState.getTicks());
477            drawRangeGridlines(g2, dataArea, rangeAxisState.getTicks());
478            
479            Shape originalClip = g2.getClip();
480            Composite originalComposite = g2.getComposite();
481    
482            g2.clip(dataArea);
483            g2.setComposite(
484                AlphaComposite.getInstance(
485                    AlphaComposite.SRC_OVER, getForegroundAlpha()
486                )
487            );
488    
489            render(g2, dataArea, info, null);
490    
491            g2.setClip(originalClip);
492            g2.setComposite(originalComposite);
493            drawOutline(g2, dataArea);
494    
495        }
496    
497        /**
498         * Draws a representation of the data within the dataArea region.  The 
499         * <code>info</code> and <code>crosshairState</code> arguments may be 
500         * <code>null</code>.
501         *
502         * @param g2  the graphics device.
503         * @param dataArea  the region in which the data is to be drawn.
504         * @param info  an optional object for collection dimension information.
505         * @param crosshairState  collects crosshair information (<code>null</code>
506         *                        permitted).
507         */
508        public void render(Graphics2D g2, Rectangle2D dataArea,
509                           PlotRenderingInfo info, CrosshairState crosshairState) {
510        
511     
512            //long start = System.currentTimeMillis();
513            //System.out.println("Start: " + start);
514            g2.setPaint(this.paint);
515    
516            // if the axes use a linear scale, you can uncomment the code below and
517            // switch to the alternative transX/transY calculation inside the loop 
518            // that follows - it is a little bit faster then.
519            // 
520            // int xx = (int) dataArea.getMinX();
521            // int ww = (int) dataArea.getWidth();
522            // int yy = (int) dataArea.getMaxY();
523            // int hh = (int) dataArea.getHeight();
524            // double domainMin = this.domainAxis.getLowerBound();
525            // double domainLength = this.domainAxis.getUpperBound() - domainMin;
526            // double rangeMin = this.rangeAxis.getLowerBound();
527            // double rangeLength = this.rangeAxis.getUpperBound() - rangeMin;
528    
529            if (this.data != null) {
530                for (int i = 0; i < this.data[0].length; i++) {
531                    float x = this.data[0][i];
532                    float y = this.data[1][i];
533                    
534                    //int transX = (int) (xx + ww * (x - domainMin) / domainLength);
535                    //int transY = (int) (yy - hh * (y - rangeMin) / rangeLength); 
536                    int transX = (int) this.domainAxis.valueToJava2D(
537                        x, dataArea, RectangleEdge.BOTTOM
538                    );
539                    int transY = (int) this.rangeAxis.valueToJava2D(
540                        y, dataArea, RectangleEdge.LEFT
541                    );
542                    g2.fillRect(transX, transY, 1, 1);
543                }
544            }
545            //long finish = System.currentTimeMillis();
546            //System.out.println("Finish: " + finish);
547            //System.out.println("Time: " + (finish - start));
548    
549        }
550    
551        /**
552         * Draws the gridlines for the plot, if they are visible.
553         *
554         * @param g2  the graphics device.
555         * @param dataArea  the data area.
556         * @param ticks  the ticks.
557         */
558        protected void drawDomainGridlines(Graphics2D g2, Rectangle2D dataArea, 
559                                           List ticks) {
560    
561            // draw the domain grid lines, if any...
562            if (isDomainGridlinesVisible()) {
563                Stroke gridStroke = getDomainGridlineStroke();
564                Paint gridPaint = getDomainGridlinePaint();
565                if ((gridStroke != null) && (gridPaint != null)) {
566                    Iterator iterator = ticks.iterator();
567                    while (iterator.hasNext()) {
568                        ValueTick tick = (ValueTick) iterator.next();
569                        double v = this.domainAxis.valueToJava2D(
570                            tick.getValue(), dataArea, RectangleEdge.BOTTOM
571                        );
572                        Line2D line = new Line2D.Double(
573                            v, dataArea.getMinY(), v, dataArea.getMaxY()
574                        );
575                        g2.setPaint(gridPaint);
576                        g2.setStroke(gridStroke);
577                        g2.draw(line);                
578                    }
579                }
580            }
581        }
582        
583        /**
584         * Draws the gridlines for the plot, if they are visible.
585         *
586         * @param g2  the graphics device.
587         * @param dataArea  the data area.
588         * @param ticks  the ticks.
589         */
590        protected void drawRangeGridlines(Graphics2D g2, Rectangle2D dataArea, 
591                                          List ticks) {
592    
593            // draw the range grid lines, if any...
594            if (isRangeGridlinesVisible()) {
595                Stroke gridStroke = getRangeGridlineStroke();
596                Paint gridPaint = getRangeGridlinePaint();
597                if ((gridStroke != null) && (gridPaint != null)) {
598                    Iterator iterator = ticks.iterator();
599                    while (iterator.hasNext()) {
600                        ValueTick tick = (ValueTick) iterator.next();
601                        double v = this.rangeAxis.valueToJava2D(
602                            tick.getValue(), dataArea, RectangleEdge.LEFT
603                        );
604                        Line2D line = new Line2D.Double(
605                            dataArea.getMinX(), v, dataArea.getMaxX(), v
606                        );
607                        g2.setPaint(gridPaint);
608                        g2.setStroke(gridStroke);
609                        g2.draw(line);                
610                    }
611                }
612            }
613    
614        }
615    
616        /**
617         * Returns the range of data values to be plotted along the axis.
618         *
619         * @param axis  the axis.
620         *
621         * @return The range.
622         */
623        public Range getDataRange(ValueAxis axis) {
624    
625            Range result = null;
626            if (axis == this.domainAxis) {
627                result = this.xDataRange;
628            }
629            else if (axis == this.rangeAxis) {
630                result = this.yDataRange;
631            }
632            return result;
633        }
634    
635        /**
636         * Calculates the X data range.
637         *
638         * @param data  the data.
639         *
640         * @return The range.
641         */
642        private Range calculateXDataRange(float[][] data) {
643            
644            Range result = null;
645            
646            if (data != null) {
647                float lowest = Float.POSITIVE_INFINITY;
648                float highest = Float.NEGATIVE_INFINITY;
649                for (int i = 0; i < data[0].length; i++) {
650                    float v = data[0][i];
651                    if (v < lowest) {
652                        lowest = v;
653                    }
654                    if (v > highest) {
655                        highest = v;
656                    }
657                }
658                if (lowest <= highest) {
659                    result = new Range(lowest, highest);
660                }
661            }
662            
663            return result;
664            
665        }
666    
667        /**
668         * Calculates the Y data range.
669         *
670         * @param data  the data.
671         *
672         * @return The range.
673         */
674        private Range calculateYDataRange(float[][] data) {
675            
676            Range result = null;
677            
678            if (data != null) {
679                float lowest = Float.POSITIVE_INFINITY;
680                float highest = Float.NEGATIVE_INFINITY;
681                for (int i = 0; i < data[0].length; i++) {
682                    float v = data[1][i];
683                    if (v < lowest) {
684                        lowest = v;
685                    }
686                    if (v > highest) {
687                        highest = v;
688                    }
689                }
690                if (lowest <= highest) {
691                    result = new Range(lowest, highest);
692                }
693            }
694            return result;
695            
696        }
697    
698        /**
699         * Multiplies the range on the domain axis/axes by the specified factor.
700         *
701         * @param factor  the zoom factor.
702         * @param info  the plot rendering info.
703         * @param source  the source point.
704         */
705        public void zoomDomainAxes(double factor, PlotRenderingInfo info, 
706                                   Point2D source) {
707            this.domainAxis.resizeRange(factor);
708        }
709    
710        /**
711         * Zooms in on the domain axes.
712         * 
713         * @param lowerPercent  the new lower bound as a percentage of the current 
714         *                      range.
715         * @param upperPercent  the new upper bound as a percentage of the current
716         *                      range.
717         * @param info  the plot rendering info.
718         * @param source  the source point.
719         */
720        public void zoomDomainAxes(double lowerPercent, double upperPercent, 
721                                   PlotRenderingInfo info, Point2D source) {
722            this.domainAxis.zoomRange(lowerPercent, upperPercent);
723        }
724    
725        /**
726         * Multiplies the range on the range axis/axes by the specified factor.
727         *
728         * @param factor  the zoom factor.
729         * @param info  the plot rendering info.
730         * @param source  the source point.
731         */
732        public void zoomRangeAxes(double factor,
733                                  PlotRenderingInfo info, Point2D source) {
734            this.rangeAxis.resizeRange(factor);
735        }
736    
737        /**
738         * Zooms in on the range axes.
739         * 
740         * @param lowerPercent  the new lower bound as a percentage of the current 
741         *                      range.
742         * @param upperPercent  the new upper bound as a percentage of the current 
743         *                      range.
744         * @param info  the plot rendering info.
745         * @param source  the source point.
746         */
747        public void zoomRangeAxes(double lowerPercent, double upperPercent,
748                                  PlotRenderingInfo info, Point2D source) {
749            this.rangeAxis.zoomRange(lowerPercent, upperPercent);
750        }
751    
752        /**
753         * Returns <code>true</code>.
754         * 
755         * @return A boolean.
756         */
757        public boolean isDomainZoomable() {
758            return true;
759        }
760        
761        /**
762         * Returns <code>true</code>.
763         * 
764         * @return A boolean.
765         */
766        public boolean isRangeZoomable() {
767            return true;
768        }
769    
770        /**
771         * Tests an object for equality with this instance.
772         * 
773         * @param obj  the object (<code>null</code> permitted).
774         * 
775         * @return A boolean.
776         */
777        public boolean equals(Object obj) {
778            if (obj == this) {
779                return true;
780            }
781            if (!super.equals(obj)) {
782                return false;
783            }
784            if (!(obj instanceof FastScatterPlot)) {
785                return false;
786            }
787            FastScatterPlot that = (FastScatterPlot) obj;
788            if (!ArrayUtilities.equal(this.data, that.data)) {
789                return false;
790            }
791            if (!ObjectUtilities.equal(this.domainAxis, that.domainAxis)) {
792                return false;
793            }
794            if (!ObjectUtilities.equal(this.rangeAxis, that.rangeAxis)) {
795                return false;
796            }
797            if (!PaintUtilities.equal(this.paint, that.paint)) {
798                return false;
799            }
800            if (this.domainGridlinesVisible != that.domainGridlinesVisible) {
801                return false;
802            }
803            if (!PaintUtilities.equal(this.domainGridlinePaint, 
804                    that.domainGridlinePaint)) {
805                return false;
806            }
807            if (!ObjectUtilities.equal(this.domainGridlineStroke, 
808                    that.domainGridlineStroke)) {
809                return false;
810            }  
811            if (!this.rangeGridlinesVisible == that.rangeGridlinesVisible) {
812                return false;
813            }
814            if (!PaintUtilities.equal(this.rangeGridlinePaint, 
815                    that.rangeGridlinePaint)) {
816                return false;
817            }
818            if (!ObjectUtilities.equal(this.rangeGridlineStroke, 
819                    that.rangeGridlineStroke)) {
820                return false;
821            }              
822            return true;
823        }
824        
825        /**
826         * Returns a clone of the plot.
827         * 
828         * @return A clone.
829         * 
830         * @throws CloneNotSupportedException if some component of the plot does 
831         *                                    not support cloning.
832         */
833        public Object clone() throws CloneNotSupportedException {
834        
835            FastScatterPlot clone = (FastScatterPlot) super.clone();    
836            
837            if (this.data != null) {
838                clone.data = ArrayUtilities.clone(this.data);    
839            }
840            
841            if (this.domainAxis != null) {
842                clone.domainAxis = (ValueAxis) this.domainAxis.clone();
843                clone.domainAxis.setPlot(clone);
844                clone.domainAxis.addChangeListener(clone);
845            }
846            
847            if (this.rangeAxis != null) {
848                clone.rangeAxis = (ValueAxis) this.rangeAxis.clone();
849                clone.rangeAxis.setPlot(clone);
850                clone.rangeAxis.addChangeListener(clone);
851            }
852                
853            return clone;
854            
855        }
856    
857        /**
858         * Provides serialization support.
859         *
860         * @param stream  the output stream.
861         *
862         * @throws IOException  if there is an I/O error.
863         */
864        private void writeObject(ObjectOutputStream stream) throws IOException {
865            stream.defaultWriteObject();
866            SerialUtilities.writePaint(this.paint, stream);
867            SerialUtilities.writeStroke(this.domainGridlineStroke, stream);
868            SerialUtilities.writePaint(this.domainGridlinePaint, stream);
869            SerialUtilities.writeStroke(this.rangeGridlineStroke, stream);
870            SerialUtilities.writePaint(this.rangeGridlinePaint, stream);
871        }
872    
873        /**
874         * Provides serialization support.
875         *
876         * @param stream  the input stream.
877         *
878         * @throws IOException  if there is an I/O error.
879         * @throws ClassNotFoundException  if there is a classpath problem.
880         */
881        private void readObject(ObjectInputStream stream) 
882                throws IOException, ClassNotFoundException {
883            stream.defaultReadObject();
884    
885            this.paint = SerialUtilities.readPaint(stream);
886            this.domainGridlineStroke = SerialUtilities.readStroke(stream);
887            this.domainGridlinePaint = SerialUtilities.readPaint(stream);
888    
889            this.rangeGridlineStroke = SerialUtilities.readStroke(stream);
890            this.rangeGridlinePaint = SerialUtilities.readPaint(stream);
891    
892            if (this.domainAxis != null) {
893                this.domainAxis.addChangeListener(this);
894            }
895    
896            if (this.rangeAxis != null) {
897                this.rangeAxis.addChangeListener(this);
898            }
899        }
900        
901    }