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