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