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