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