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