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
061package org.jfree.chart.plot;
062
063import java.awt.AlphaComposite;
064import java.awt.BasicStroke;
065import java.awt.Color;
066import java.awt.Composite;
067import java.awt.Graphics2D;
068import java.awt.Paint;
069import java.awt.Shape;
070import java.awt.Stroke;
071import java.awt.geom.Line2D;
072import java.awt.geom.Point2D;
073import java.awt.geom.Rectangle2D;
074import java.io.IOException;
075import java.io.ObjectInputStream;
076import java.io.ObjectOutputStream;
077import java.io.Serializable;
078import java.util.Iterator;
079import java.util.List;
080import java.util.ResourceBundle;
081
082import org.jfree.chart.axis.AxisSpace;
083import org.jfree.chart.axis.AxisState;
084import org.jfree.chart.axis.NumberAxis;
085import org.jfree.chart.axis.ValueAxis;
086import org.jfree.chart.axis.ValueTick;
087import org.jfree.chart.event.PlotChangeEvent;
088import org.jfree.data.Range;
089import org.jfree.io.SerialUtilities;
090import org.jfree.ui.RectangleEdge;
091import org.jfree.ui.RectangleInsets;
092import org.jfree.util.ArrayUtilities;
093import org.jfree.util.ObjectUtilities;
094import org.jfree.util.PaintUtilities;
095
096/**
097 * A fast scatter plot.
098 */
099public 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}