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