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