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