001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * --------------
028 * ValueAxis.java
029 * --------------
030 * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Jonathan Nash;
034 *                   Nicolas Brodu (for Astrium and EADS Corporate Research
035 *                   Center);
036 *                   Peter Kolb (patch 1934255);
037 *                   Andrew Mickish (patch 1870189);
038 *
039 * Changes
040 * -------
041 * 18-Sep-2001 : Added standard header and fixed DOS encoding problem (DG);
042 * 23-Nov-2001 : Overhauled standard tick unit code (DG);
043 * 04-Dec-2001 : Changed constructors to protected, and tidied up default
044 *               values (DG);
045 * 12-Dec-2001 : Fixed vertical gridlines bug (DG);
046 * 16-Jan-2002 : Added an optional crosshair, based on the implementation by
047 *               Jonathan Nash (DG);
048 * 23-Jan-2002 : Moved the minimum and maximum values to here from NumberAxis,
049 *               and changed the type from Number to double (DG);
050 * 25-Feb-2002 : Added default value for autoRange. Changed autoAdjustRange
051 *               from public to protected. Updated import statements (DG);
052 * 23-Apr-2002 : Added setRange() method (DG);
053 * 29-Apr-2002 : Added range adjustment methods (DG);
054 * 13-Jun-2002 : Modified setCrosshairValue() to notify listeners only when the
055 *               crosshairs are visible, to avoid unnecessary repaints, as
056 *               suggested by Kees Kuip (DG);
057 * 25-Jul-2002 : Moved lower and upper margin attributes from the NumberAxis
058 *               class (DG);
059 * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);
060 * 01-Oct-2002 : Fixed errors reported by Checkstyle (DG);
061 * 04-Oct-2002 : Moved standardTickUnits from NumberAxis --> ValueAxis (DG);
062 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
063 * 19-Nov-2002 : Removed grid settings (now controlled by the plot) (DG);
064 * 27-Nov-2002 : Moved the 'inverted' attributed from NumberAxis to
065 *               ValueAxis (DG);
066 * 03-Jan-2003 : Small fix to ensure auto-range minimum is observed
067 *               immediately (DG);
068 * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double (DG);
069 * 20-Jan-2003 : Replaced monolithic constructor (DG);
070 * 26-Mar-2003 : Implemented Serializable (DG);
071 * 09-May-2003 : Added AxisLocation parameter to translation methods (DG);
072 * 13-Aug-2003 : Implemented Cloneable (DG);
073 * 01-Sep-2003 : Fixed bug 793167 (setMaximumAxisValue exception) (DG);
074 * 02-Sep-2003 : Fixed bug 795366 (zooming on inverted axes) (DG);
075 * 08-Sep-2003 : Completed Serialization support (NB);
076 * 08-Sep-2003 : Renamed get/setMinimumValue --> get/setLowerBound,
077 *               and get/setMaximumValue --> get/setUpperBound (DG);
078 * 27-Oct-2003 : Changed DEFAULT_AUTO_RANGE_MINIMUM_SIZE value - see bug ID
079 *               829606 (DG);
080 * 07-Nov-2003 : Changes to tick mechanism (DG);
081 * 06-Jan-2004 : Moved axis line attributes to Axis class (DG);
082 * 21-Jan-2004 : Removed redundant axisLineVisible attribute.  Renamed
083 *               translateJava2DToValue --> java2DToValue, and
084 *               translateValueToJava2D --> valueToJava2D (DG);
085 * 23-Jan-2004 : Fixed setAxisLinePaint() and setAxisLineStroke() which had no
086 *               effect (andreas.gawecki@coremedia.com);
087 * 07-Apr-2004 : Changed text bounds calculation (DG);
088 * 26-Apr-2004 : Added getter/setter methods for arrow shapes (DG);
089 * 18-May-2004 : Added methods to set axis range *including* current
090 *               margins (DG);
091 * 02-Jun-2004 : Fixed bug in setRangeWithMargins() method (DG);
092 * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities
093 *               --> TextUtilities (DG);
094 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
095 *               release (DG);
096 * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
097 * ------------- JFREECHART 1.0.x ---------------------------------------------
098 * 10-Oct-2006 : Source reformatting (DG);
099 * 22-Mar-2007 : Added new defaultAutoRange attribute (DG);
100 * 02-Aug-2007 : Check for major tick when drawing label (DG);
101 * 25-Sep-2008 : Added minor tick support, see patch 1934255 by Peter Kolb (DG);
102 * 21-Jan-2009 : Updated default behaviour of minor ticks (DG);
103 * 18-Mar-2008 : Added resizeRange2() method which provides more natural
104 *               anchored zooming for mouse wheel support (DG);
105 * 26-Mar-2009 : In equals(), only check current range if autoRange is
106 *               false (DG);
107 * 30-Mar-2009 : Added pan(double) method (DG);
108 *
109 */
110
111package org.jfree.chart.axis;
112
113import java.awt.Font;
114import java.awt.FontMetrics;
115import java.awt.Graphics2D;
116import java.awt.Polygon;
117import java.awt.Shape;
118import java.awt.font.LineMetrics;
119import java.awt.geom.AffineTransform;
120import java.awt.geom.Line2D;
121import java.awt.geom.Rectangle2D;
122import java.io.IOException;
123import java.io.ObjectInputStream;
124import java.io.ObjectOutputStream;
125import java.io.Serializable;
126import java.util.Iterator;
127import java.util.List;
128
129import org.jfree.chart.event.AxisChangeEvent;
130import org.jfree.chart.plot.Plot;
131import org.jfree.data.Range;
132import org.jfree.io.SerialUtilities;
133import org.jfree.text.TextUtilities;
134import org.jfree.ui.RectangleEdge;
135import org.jfree.ui.RectangleInsets;
136import org.jfree.util.ObjectUtilities;
137import org.jfree.util.PublicCloneable;
138
139/**
140 * The base class for axes that display value data, where values are measured
141 * using the <code>double</code> primitive.  The two key subclasses are
142 * {@link DateAxis} and {@link NumberAxis}.
143 */
144public abstract class ValueAxis extends Axis
145        implements Cloneable, PublicCloneable, Serializable {
146
147    /** For serialization. */
148    private static final long serialVersionUID = 3698345477322391456L;
149
150    /** The default axis range. */
151    public static final Range DEFAULT_RANGE = new Range(0.0, 1.0);
152
153    /** The default auto-range value. */
154    public static final boolean DEFAULT_AUTO_RANGE = true;
155
156    /** The default inverted flag setting. */
157    public static final boolean DEFAULT_INVERTED = false;
158
159    /** The default minimum auto range. */
160    public static final double DEFAULT_AUTO_RANGE_MINIMUM_SIZE = 0.00000001;
161
162    /** The default value for the lower margin (0.05 = 5%). */
163    public static final double DEFAULT_LOWER_MARGIN = 0.05;
164
165    /** The default value for the upper margin (0.05 = 5%). */
166    public static final double DEFAULT_UPPER_MARGIN = 0.05;
167
168    /**
169     * The default lower bound for the axis.
170     *
171     * @deprecated From 1.0.5 onwards, the axis defines a defaultRange
172     *     attribute (see {@link #getDefaultAutoRange()}).
173     */
174    public static final double DEFAULT_LOWER_BOUND = 0.0;
175
176    /**
177     * The default upper bound for the axis.
178     *
179     * @deprecated From 1.0.5 onwards, the axis defines a defaultRange
180     *     attribute (see {@link #getDefaultAutoRange()}).
181     */
182    public static final double DEFAULT_UPPER_BOUND = 1.0;
183
184    /** The default auto-tick-unit-selection value. */
185    public static final boolean DEFAULT_AUTO_TICK_UNIT_SELECTION = true;
186
187    /** The maximum tick count. */
188    public static final int MAXIMUM_TICK_COUNT = 500;
189
190    /**
191     * A flag that controls whether an arrow is drawn at the positive end of
192     * the axis line.
193     */
194    private boolean positiveArrowVisible;
195
196    /**
197     * A flag that controls whether an arrow is drawn at the negative end of
198     * the axis line.
199     */
200    private boolean negativeArrowVisible;
201
202    /** The shape used for an up arrow. */
203    private transient Shape upArrow;
204
205    /** The shape used for a down arrow. */
206    private transient Shape downArrow;
207
208    /** The shape used for a left arrow. */
209    private transient Shape leftArrow;
210
211    /** The shape used for a right arrow. */
212    private transient Shape rightArrow;
213
214    /** A flag that affects the orientation of the values on the axis. */
215    private boolean inverted;
216
217    /** The axis range. */
218    private Range range;
219
220    /**
221     * Flag that indicates whether the axis automatically scales to fit the
222     * chart data.
223     */
224    private boolean autoRange;
225
226    /** The minimum size for the 'auto' axis range (excluding margins). */
227    private double autoRangeMinimumSize;
228
229    /**
230     * The default range is used when the dataset is empty and the axis needs
231     * to determine the auto range.
232     *
233     * @since 1.0.5
234     */
235    private Range defaultAutoRange;
236
237    /**
238     * The upper margin percentage.  This indicates the amount by which the
239     * maximum axis value exceeds the maximum data value (as a percentage of
240     * the range on the axis) when the axis range is determined automatically.
241     */
242    private double upperMargin;
243
244    /**
245     * The lower margin.  This is a percentage that indicates the amount by
246     * which the minimum axis value is "less than" the minimum data value when
247     * the axis range is determined automatically.
248     */
249    private double lowerMargin;
250
251    /**
252     * If this value is positive, the amount is subtracted from the maximum
253     * data value to determine the lower axis range.  This can be used to
254     * provide a fixed "window" on dynamic data.
255     */
256    private double fixedAutoRange;
257
258    /**
259     * Flag that indicates whether or not the tick unit is selected
260     * automatically.
261     */
262    private boolean autoTickUnitSelection;
263
264    /** The standard tick units for the axis. */
265    private TickUnitSource standardTickUnits;
266
267    /** An index into an array of standard tick values. */
268    private int autoTickIndex;
269
270    /**
271     * The number of minor ticks per major tick unit.  This is an override
272     * field, if the value is > 0 it is used, otherwise the axis refers to the
273     * minorTickCount in the current tickUnit.
274     */
275    private int minorTickCount;
276
277    /** A flag indicating whether or not tick labels are rotated to vertical. */
278    private boolean verticalTickLabels;
279
280    /**
281     * Constructs a value axis.
282     *
283     * @param label  the axis label (<code>null</code> permitted).
284     * @param standardTickUnits  the source for standard tick units
285     *                           (<code>null</code> permitted).
286     */
287    protected ValueAxis(String label, TickUnitSource standardTickUnits) {
288
289        super(label);
290
291        this.positiveArrowVisible = false;
292        this.negativeArrowVisible = false;
293
294        this.range = DEFAULT_RANGE;
295        this.autoRange = DEFAULT_AUTO_RANGE;
296        this.defaultAutoRange = DEFAULT_RANGE;
297
298        this.inverted = DEFAULT_INVERTED;
299        this.autoRangeMinimumSize = DEFAULT_AUTO_RANGE_MINIMUM_SIZE;
300
301        this.lowerMargin = DEFAULT_LOWER_MARGIN;
302        this.upperMargin = DEFAULT_UPPER_MARGIN;
303
304        this.fixedAutoRange = 0.0;
305
306        this.autoTickUnitSelection = DEFAULT_AUTO_TICK_UNIT_SELECTION;
307        this.standardTickUnits = standardTickUnits;
308
309        Polygon p1 = new Polygon();
310        p1.addPoint(0, 0);
311        p1.addPoint(-2, 2);
312        p1.addPoint(2, 2);
313
314        this.upArrow = p1;
315
316        Polygon p2 = new Polygon();
317        p2.addPoint(0, 0);
318        p2.addPoint(-2, -2);
319        p2.addPoint(2, -2);
320
321        this.downArrow = p2;
322
323        Polygon p3 = new Polygon();
324        p3.addPoint(0, 0);
325        p3.addPoint(-2, -2);
326        p3.addPoint(-2, 2);
327
328        this.rightArrow = p3;
329
330        Polygon p4 = new Polygon();
331        p4.addPoint(0, 0);
332        p4.addPoint(2, -2);
333        p4.addPoint(2, 2);
334
335        this.leftArrow = p4;
336
337        this.verticalTickLabels = false;
338        this.minorTickCount = 0;
339
340    }
341
342    /**
343     * Returns <code>true</code> if the tick labels should be rotated (to
344     * vertical), and <code>false</code> otherwise.
345     *
346     * @return <code>true</code> or <code>false</code>.
347     *
348     * @see #setVerticalTickLabels(boolean)
349     */
350    public boolean isVerticalTickLabels() {
351        return this.verticalTickLabels;
352    }
353
354    /**
355     * Sets the flag that controls whether the tick labels are displayed
356     * vertically (that is, rotated 90 degrees from horizontal).  If the flag
357     * is changed, an {@link AxisChangeEvent} is sent to all registered
358     * listeners.
359     *
360     * @param flag  the flag.
361     *
362     * @see #isVerticalTickLabels()
363     */
364    public void setVerticalTickLabels(boolean flag) {
365        if (this.verticalTickLabels != flag) {
366            this.verticalTickLabels = flag;
367            notifyListeners(new AxisChangeEvent(this));
368        }
369    }
370
371    /**
372     * Returns a flag that controls whether or not the axis line has an arrow
373     * drawn that points in the positive direction for the axis.
374     *
375     * @return A boolean.
376     *
377     * @see #setPositiveArrowVisible(boolean)
378     */
379    public boolean isPositiveArrowVisible() {
380        return this.positiveArrowVisible;
381    }
382
383    /**
384     * Sets a flag that controls whether or not the axis lines has an arrow
385     * drawn that points in the positive direction for the axis, and sends an
386     * {@link AxisChangeEvent} to all registered listeners.
387     *
388     * @param visible  the flag.
389     *
390     * @see #isPositiveArrowVisible()
391     */
392    public void setPositiveArrowVisible(boolean visible) {
393        this.positiveArrowVisible = visible;
394        notifyListeners(new AxisChangeEvent(this));
395    }
396
397    /**
398     * Returns a flag that controls whether or not the axis line has an arrow
399     * drawn that points in the negative direction for the axis.
400     *
401     * @return A boolean.
402     *
403     * @see #setNegativeArrowVisible(boolean)
404     */
405    public boolean isNegativeArrowVisible() {
406        return this.negativeArrowVisible;
407    }
408
409    /**
410     * Sets a flag that controls whether or not the axis lines has an arrow
411     * drawn that points in the negative direction for the axis, and sends an
412     * {@link AxisChangeEvent} to all registered listeners.
413     *
414     * @param visible  the flag.
415     *
416     * @see #setNegativeArrowVisible(boolean)
417     */
418    public void setNegativeArrowVisible(boolean visible) {
419        this.negativeArrowVisible = visible;
420        notifyListeners(new AxisChangeEvent(this));
421    }
422
423    /**
424     * Returns a shape that can be displayed as an arrow pointing upwards at
425     * the end of an axis line.
426     *
427     * @return A shape (never <code>null</code>).
428     *
429     * @see #setUpArrow(Shape)
430     */
431    public Shape getUpArrow() {
432        return this.upArrow;
433    }
434
435    /**
436     * Sets the shape that can be displayed as an arrow pointing upwards at
437     * the end of an axis line and sends an {@link AxisChangeEvent} to all
438     * registered listeners.
439     *
440     * @param arrow  the arrow shape (<code>null</code> not permitted).
441     *
442     * @see #getUpArrow()
443     */
444    public void setUpArrow(Shape arrow) {
445        if (arrow == null) {
446            throw new IllegalArgumentException("Null 'arrow' argument.");
447        }
448        this.upArrow = arrow;
449        notifyListeners(new AxisChangeEvent(this));
450    }
451
452    /**
453     * Returns a shape that can be displayed as an arrow pointing downwards at
454     * the end of an axis line.
455     *
456     * @return A shape (never <code>null</code>).
457     *
458     * @see #setDownArrow(Shape)
459     */
460    public Shape getDownArrow() {
461        return this.downArrow;
462    }
463
464    /**
465     * Sets the shape that can be displayed as an arrow pointing downwards at
466     * the end of an axis line and sends an {@link AxisChangeEvent} to all
467     * registered listeners.
468     *
469     * @param arrow  the arrow shape (<code>null</code> not permitted).
470     *
471     * @see #getDownArrow()
472     */
473    public void setDownArrow(Shape arrow) {
474        if (arrow == null) {
475            throw new IllegalArgumentException("Null 'arrow' argument.");
476        }
477        this.downArrow = arrow;
478        notifyListeners(new AxisChangeEvent(this));
479    }
480
481    /**
482     * Returns a shape that can be displayed as an arrow pointing left at the
483     * end of an axis line.
484     *
485     * @return A shape (never <code>null</code>).
486     *
487     * @see #setLeftArrow(Shape)
488     */
489    public Shape getLeftArrow() {
490        return this.leftArrow;
491    }
492
493    /**
494     * Sets the shape that can be displayed as an arrow pointing left at the
495     * end of an axis line and sends an {@link AxisChangeEvent} to all
496     * registered listeners.
497     *
498     * @param arrow  the arrow shape (<code>null</code> not permitted).
499     *
500     * @see #getLeftArrow()
501     */
502    public void setLeftArrow(Shape arrow) {
503        if (arrow == null) {
504            throw new IllegalArgumentException("Null 'arrow' argument.");
505        }
506        this.leftArrow = arrow;
507        notifyListeners(new AxisChangeEvent(this));
508    }
509
510    /**
511     * Returns a shape that can be displayed as an arrow pointing right at the
512     * end of an axis line.
513     *
514     * @return A shape (never <code>null</code>).
515     *
516     * @see #setRightArrow(Shape)
517     */
518    public Shape getRightArrow() {
519        return this.rightArrow;
520    }
521
522    /**
523     * Sets the shape that can be displayed as an arrow pointing rightwards at
524     * the end of an axis line and sends an {@link AxisChangeEvent} to all
525     * registered listeners.
526     *
527     * @param arrow  the arrow shape (<code>null</code> not permitted).
528     *
529     * @see #getRightArrow()
530     */
531    public void setRightArrow(Shape arrow) {
532        if (arrow == null) {
533            throw new IllegalArgumentException("Null 'arrow' argument.");
534        }
535        this.rightArrow = arrow;
536        notifyListeners(new AxisChangeEvent(this));
537    }
538
539    /**
540     * Draws an axis line at the current cursor position and edge.
541     *
542     * @param g2  the graphics device.
543     * @param cursor  the cursor position.
544     * @param dataArea  the data area.
545     * @param edge  the edge.
546     */
547    protected void drawAxisLine(Graphics2D g2, double cursor,
548                                Rectangle2D dataArea, RectangleEdge edge) {
549        Line2D axisLine = null;
550        if (edge == RectangleEdge.TOP) {
551            axisLine = new Line2D.Double(dataArea.getX(), cursor,
552                    dataArea.getMaxX(), cursor);
553        }
554        else if (edge == RectangleEdge.BOTTOM) {
555            axisLine = new Line2D.Double(dataArea.getX(), cursor,
556                    dataArea.getMaxX(), cursor);
557        }
558        else if (edge == RectangleEdge.LEFT) {
559            axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
560                    dataArea.getMaxY());
561        }
562        else if (edge == RectangleEdge.RIGHT) {
563            axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
564                    dataArea.getMaxY());
565        }
566        g2.setPaint(getAxisLinePaint());
567        g2.setStroke(getAxisLineStroke());
568        g2.draw(axisLine);
569
570        boolean drawUpOrRight = false;
571        boolean drawDownOrLeft = false;
572        if (this.positiveArrowVisible) {
573            if (this.inverted) {
574                drawDownOrLeft = true;
575            }
576            else {
577                drawUpOrRight = true;
578            }
579        }
580        if (this.negativeArrowVisible) {
581            if (this.inverted) {
582                drawUpOrRight = true;
583            }
584            else {
585                drawDownOrLeft = true;
586            }
587        }
588        if (drawUpOrRight) {
589            double x = 0.0;
590            double y = 0.0;
591            Shape arrow = null;
592            if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
593                x = dataArea.getMaxX();
594                y = cursor;
595                arrow = this.rightArrow;
596            }
597            else if (edge == RectangleEdge.LEFT
598                    || edge == RectangleEdge.RIGHT) {
599                x = cursor;
600                y = dataArea.getMinY();
601                arrow = this.upArrow;
602            }
603
604            // draw the arrow...
605            AffineTransform transformer = new AffineTransform();
606            transformer.setToTranslation(x, y);
607            Shape shape = transformer.createTransformedShape(arrow);
608            g2.fill(shape);
609            g2.draw(shape);
610        }
611
612        if (drawDownOrLeft) {
613            double x = 0.0;
614            double y = 0.0;
615            Shape arrow = null;
616            if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
617                x = dataArea.getMinX();
618                y = cursor;
619                arrow = this.leftArrow;
620            }
621            else if (edge == RectangleEdge.LEFT
622                    || edge == RectangleEdge.RIGHT) {
623                x = cursor;
624                y = dataArea.getMaxY();
625                arrow = this.downArrow;
626            }
627
628            // draw the arrow...
629            AffineTransform transformer = new AffineTransform();
630            transformer.setToTranslation(x, y);
631            Shape shape = transformer.createTransformedShape(arrow);
632            g2.fill(shape);
633            g2.draw(shape);
634        }
635
636    }
637
638    /**
639     * Calculates the anchor point for a tick label.
640     *
641     * @param tick  the tick.
642     * @param cursor  the cursor.
643     * @param dataArea  the data area.
644     * @param edge  the edge on which the axis is drawn.
645     *
646     * @return The x and y coordinates of the anchor point.
647     */
648    protected float[] calculateAnchorPoint(ValueTick tick,
649                                           double cursor,
650                                           Rectangle2D dataArea,
651                                           RectangleEdge edge) {
652
653        RectangleInsets insets = getTickLabelInsets();
654        float[] result = new float[2];
655        if (edge == RectangleEdge.TOP) {
656            result[0] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
657            result[1] = (float) (cursor - insets.getBottom() - 2.0);
658        }
659        else if (edge == RectangleEdge.BOTTOM) {
660            result[0] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
661            result[1] = (float) (cursor + insets.getTop() + 2.0);
662        }
663        else if (edge == RectangleEdge.LEFT) {
664            result[0] = (float) (cursor - insets.getLeft() - 2.0);
665            result[1] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
666        }
667        else if (edge == RectangleEdge.RIGHT) {
668            result[0] = (float) (cursor + insets.getRight() + 2.0);
669            result[1] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
670        }
671        return result;
672    }
673
674    /**
675     * Draws the axis line, tick marks and tick mark labels.
676     *
677     * @param g2  the graphics device.
678     * @param cursor  the cursor.
679     * @param plotArea  the plot area.
680     * @param dataArea  the data area.
681     * @param edge  the edge that the axis is aligned with.
682     *
683     * @return The width or height used to draw the axis.
684     */
685    protected AxisState drawTickMarksAndLabels(Graphics2D g2,
686            double cursor, Rectangle2D plotArea, Rectangle2D dataArea,
687            RectangleEdge edge) {
688
689        AxisState state = new AxisState(cursor);
690
691        if (isAxisLineVisible()) {
692            drawAxisLine(g2, cursor, dataArea, edge);
693        }
694
695        List ticks = refreshTicks(g2, state, dataArea, edge);
696        state.setTicks(ticks);
697        g2.setFont(getTickLabelFont());
698        Iterator iterator = ticks.iterator();
699        while (iterator.hasNext()) {
700            ValueTick tick = (ValueTick) iterator.next();
701            if (isTickLabelsVisible()) {
702                g2.setPaint(getTickLabelPaint());
703                float[] anchorPoint = calculateAnchorPoint(tick, cursor,
704                        dataArea, edge);
705                TextUtilities.drawRotatedString(tick.getText(), g2,
706                        anchorPoint[0], anchorPoint[1], tick.getTextAnchor(),
707                        tick.getAngle(), tick.getRotationAnchor());
708            }
709
710            if ((isTickMarksVisible() && tick.getTickType().equals(
711                    TickType.MAJOR)) || (isMinorTickMarksVisible()
712                    && tick.getTickType().equals(TickType.MINOR))) {
713
714                double ol = (tick.getTickType().equals(TickType.MINOR)) ?
715                    getMinorTickMarkOutsideLength() : getTickMarkOutsideLength();
716
717                double il = (tick.getTickType().equals(TickType.MINOR)) ?
718                    getMinorTickMarkInsideLength() : getTickMarkInsideLength();
719
720                float xx = (float) valueToJava2D(tick.getValue(), dataArea,
721                        edge);
722                Line2D mark = null;
723                g2.setStroke(getTickMarkStroke());
724                g2.setPaint(getTickMarkPaint());
725                if (edge == RectangleEdge.LEFT) {
726                    mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
727                }
728                else if (edge == RectangleEdge.RIGHT) {
729                    mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
730                }
731                else if (edge == RectangleEdge.TOP) {
732                    mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
733                }
734                else if (edge == RectangleEdge.BOTTOM) {
735                    mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
736                }
737                g2.draw(mark);
738            }
739        }
740
741        // need to work out the space used by the tick labels...
742        // so we can update the cursor...
743        double used = 0.0;
744        if (isTickLabelsVisible()) {
745            if (edge == RectangleEdge.LEFT) {
746                used += findMaximumTickLabelWidth(ticks, g2, plotArea,
747                        isVerticalTickLabels());
748                state.cursorLeft(used);
749            }
750            else if (edge == RectangleEdge.RIGHT) {
751                used = findMaximumTickLabelWidth(ticks, g2, plotArea,
752                        isVerticalTickLabels());
753                state.cursorRight(used);
754            }
755            else if (edge == RectangleEdge.TOP) {
756                used = findMaximumTickLabelHeight(ticks, g2, plotArea,
757                        isVerticalTickLabels());
758                state.cursorUp(used);
759            }
760            else if (edge == RectangleEdge.BOTTOM) {
761                used = findMaximumTickLabelHeight(ticks, g2, plotArea,
762                        isVerticalTickLabels());
763                state.cursorDown(used);
764            }
765        }
766
767        return state;
768    }
769
770    /**
771     * Returns the space required to draw the axis.
772     *
773     * @param g2  the graphics device.
774     * @param plot  the plot that the axis belongs to.
775     * @param plotArea  the area within which the plot should be drawn.
776     * @param edge  the axis location.
777     * @param space  the space already reserved (for other axes).
778     *
779     * @return The space required to draw the axis (including pre-reserved
780     *         space).
781     */
782    public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
783                                  Rectangle2D plotArea,
784                                  RectangleEdge edge, AxisSpace space) {
785
786        // create a new space object if one wasn't supplied...
787        if (space == null) {
788            space = new AxisSpace();
789        }
790
791        // if the axis is not visible, no additional space is required...
792        if (!isVisible()) {
793            return space;
794        }
795
796        // if the axis has a fixed dimension, return it...
797        double dimension = getFixedDimension();
798        if (dimension > 0.0) {
799            space.ensureAtLeast(dimension, edge);
800        }
801
802        // calculate the max size of the tick labels (if visible)...
803        double tickLabelHeight = 0.0;
804        double tickLabelWidth = 0.0;
805        if (isTickLabelsVisible()) {
806            g2.setFont(getTickLabelFont());
807            List ticks = refreshTicks(g2, new AxisState(), plotArea, edge);
808            if (RectangleEdge.isTopOrBottom(edge)) {
809                tickLabelHeight = findMaximumTickLabelHeight(ticks, g2,
810                        plotArea, isVerticalTickLabels());
811            }
812            else if (RectangleEdge.isLeftOrRight(edge)) {
813                tickLabelWidth = findMaximumTickLabelWidth(ticks, g2, plotArea,
814                        isVerticalTickLabels());
815            }
816        }
817
818        // get the axis label size and update the space object...
819        Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
820        double labelHeight = 0.0;
821        double labelWidth = 0.0;
822        if (RectangleEdge.isTopOrBottom(edge)) {
823            labelHeight = labelEnclosure.getHeight();
824            space.add(labelHeight + tickLabelHeight, edge);
825        }
826        else if (RectangleEdge.isLeftOrRight(edge)) {
827            labelWidth = labelEnclosure.getWidth();
828            space.add(labelWidth + tickLabelWidth, edge);
829        }
830
831        return space;
832
833    }
834
835    /**
836     * A utility method for determining the height of the tallest tick label.
837     *
838     * @param ticks  the ticks.
839     * @param g2  the graphics device.
840     * @param drawArea  the area within which the plot and axes should be drawn.
841     * @param vertical  a flag that indicates whether or not the tick labels
842     *                  are 'vertical'.
843     *
844     * @return The height of the tallest tick label.
845     */
846    protected double findMaximumTickLabelHeight(List ticks,
847                                                Graphics2D g2,
848                                                Rectangle2D drawArea,
849                                                boolean vertical) {
850
851        RectangleInsets insets = getTickLabelInsets();
852        Font font = getTickLabelFont();
853        double maxHeight = 0.0;
854        if (vertical) {
855            FontMetrics fm = g2.getFontMetrics(font);
856            Iterator iterator = ticks.iterator();
857            while (iterator.hasNext()) {
858                Tick tick = (Tick) iterator.next();
859                Rectangle2D labelBounds = TextUtilities.getTextBounds(
860                        tick.getText(), g2, fm);
861                if (labelBounds.getWidth() + insets.getTop()
862                        + insets.getBottom() > maxHeight) {
863                    maxHeight = labelBounds.getWidth()
864                                + insets.getTop() + insets.getBottom();
865                }
866            }
867        }
868        else {
869            LineMetrics metrics = font.getLineMetrics("ABCxyz",
870                    g2.getFontRenderContext());
871            maxHeight = metrics.getHeight()
872                        + insets.getTop() + insets.getBottom();
873        }
874        return maxHeight;
875
876    }
877
878    /**
879     * A utility method for determining the width of the widest tick label.
880     *
881     * @param ticks  the ticks.
882     * @param g2  the graphics device.
883     * @param drawArea  the area within which the plot and axes should be drawn.
884     * @param vertical  a flag that indicates whether or not the tick labels
885     *                  are 'vertical'.
886     *
887     * @return The width of the tallest tick label.
888     */
889    protected double findMaximumTickLabelWidth(List ticks,
890                                               Graphics2D g2,
891                                               Rectangle2D drawArea,
892                                               boolean vertical) {
893
894        RectangleInsets insets = getTickLabelInsets();
895        Font font = getTickLabelFont();
896        double maxWidth = 0.0;
897        if (!vertical) {
898            FontMetrics fm = g2.getFontMetrics(font);
899            Iterator iterator = ticks.iterator();
900            while (iterator.hasNext()) {
901                Tick tick = (Tick) iterator.next();
902                Rectangle2D labelBounds = TextUtilities.getTextBounds(
903                        tick.getText(), g2, fm);
904                if (labelBounds.getWidth() + insets.getLeft()
905                        + insets.getRight() > maxWidth) {
906                    maxWidth = labelBounds.getWidth()
907                               + insets.getLeft() + insets.getRight();
908                }
909            }
910        }
911        else {
912            LineMetrics metrics = font.getLineMetrics("ABCxyz",
913                    g2.getFontRenderContext());
914            maxWidth = metrics.getHeight()
915                       + insets.getTop() + insets.getBottom();
916        }
917        return maxWidth;
918
919    }
920
921    /**
922     * Returns a flag that controls the direction of values on the axis.
923     * <P>
924     * For a regular axis, values increase from left to right (for a horizontal
925     * axis) and bottom to top (for a vertical axis).  When the axis is
926     * 'inverted', the values increase in the opposite direction.
927     *
928     * @return The flag.
929     *
930     * @see #setInverted(boolean)
931     */
932    public boolean isInverted() {
933        return this.inverted;
934    }
935
936    /**
937     * Sets a flag that controls the direction of values on the axis, and
938     * notifies registered listeners that the axis has changed.
939     *
940     * @param flag  the flag.
941     *
942     * @see #isInverted()
943     */
944    public void setInverted(boolean flag) {
945
946        if (this.inverted != flag) {
947            this.inverted = flag;
948            notifyListeners(new AxisChangeEvent(this));
949        }
950
951    }
952
953    /**
954     * Returns the flag that controls whether or not the axis range is
955     * automatically adjusted to fit the data values.
956     *
957     * @return The flag.
958     *
959     * @see #setAutoRange(boolean)
960     */
961    public boolean isAutoRange() {
962        return this.autoRange;
963    }
964
965    /**
966     * Sets a flag that determines whether or not the axis range is
967     * automatically adjusted to fit the data, and notifies registered
968     * listeners that the axis has been modified.
969     *
970     * @param auto  the new value of the flag.
971     *
972     * @see #isAutoRange()
973     */
974    public void setAutoRange(boolean auto) {
975        setAutoRange(auto, true);
976    }
977
978    /**
979     * Sets the auto range attribute.  If the <code>notify</code> flag is set,
980     * an {@link AxisChangeEvent} is sent to registered listeners.
981     *
982     * @param auto  the flag.
983     * @param notify  notify listeners?
984     *
985     * @see #isAutoRange()
986     */
987    protected void setAutoRange(boolean auto, boolean notify) {
988        if (this.autoRange != auto) {
989            this.autoRange = auto;
990            if (this.autoRange) {
991                autoAdjustRange();
992            }
993            if (notify) {
994                notifyListeners(new AxisChangeEvent(this));
995            }
996        }
997    }
998
999    /**
1000     * Returns the minimum size allowed for the axis range when it is
1001     * automatically calculated.
1002     *
1003     * @return The minimum range.
1004     *
1005     * @see #setAutoRangeMinimumSize(double)
1006     */
1007    public double getAutoRangeMinimumSize() {
1008        return this.autoRangeMinimumSize;
1009    }
1010
1011    /**
1012     * Sets the auto range minimum size and sends an {@link AxisChangeEvent}
1013     * to all registered listeners.
1014     *
1015     * @param size  the size.
1016     *
1017     * @see #getAutoRangeMinimumSize()
1018     */
1019    public void setAutoRangeMinimumSize(double size) {
1020        setAutoRangeMinimumSize(size, true);
1021    }
1022
1023    /**
1024     * Sets the minimum size allowed for the axis range when it is
1025     * automatically calculated.
1026     * <p>
1027     * If requested, an {@link AxisChangeEvent} is forwarded to all registered
1028     * listeners.
1029     *
1030     * @param size  the new minimum.
1031     * @param notify  notify listeners?
1032     */
1033    public void setAutoRangeMinimumSize(double size, boolean notify) {
1034        if (size <= 0.0) {
1035            throw new IllegalArgumentException(
1036                "NumberAxis.setAutoRangeMinimumSize(double): must be > 0.0.");
1037        }
1038        if (this.autoRangeMinimumSize != size) {
1039            this.autoRangeMinimumSize = size;
1040            if (this.autoRange) {
1041                autoAdjustRange();
1042            }
1043            if (notify) {
1044                notifyListeners(new AxisChangeEvent(this));
1045            }
1046        }
1047
1048    }
1049
1050    /**
1051     * Returns the default auto range.
1052     *
1053     * @return The default auto range (never <code>null</code>).
1054     *
1055     * @see #setDefaultAutoRange(Range)
1056     *
1057     * @since 1.0.5
1058     */
1059    public Range getDefaultAutoRange() {
1060        return this.defaultAutoRange;
1061    }
1062
1063    /**
1064     * Sets the default auto range and sends an {@link AxisChangeEvent} to all
1065     * registered listeners.
1066     *
1067     * @param range  the range (<code>null</code> not permitted).
1068     *
1069     * @see #getDefaultAutoRange()
1070     *
1071     * @since 1.0.5
1072     */
1073    public void setDefaultAutoRange(Range range) {
1074        if (range == null) {
1075            throw new IllegalArgumentException("Null 'range' argument.");
1076        }
1077        this.defaultAutoRange = range;
1078        notifyListeners(new AxisChangeEvent(this));
1079    }
1080
1081    /**
1082     * Returns the lower margin for the axis, expressed as a percentage of the
1083     * axis range.  This controls the space added to the lower end of the axis
1084     * when the axis range is automatically calculated (it is ignored when the
1085     * axis range is set explicitly). The default value is 0.05 (five percent).
1086     *
1087     * @return The lower margin.
1088     *
1089     * @see #setLowerMargin(double)
1090     */
1091    public double getLowerMargin() {
1092        return this.lowerMargin;
1093    }
1094
1095    /**
1096     * Sets the lower margin for the axis (as a percentage of the axis range)
1097     * and sends an {@link AxisChangeEvent} to all registered listeners.  This
1098     * margin is added only when the axis range is auto-calculated - if you set
1099     * the axis range manually, the margin is ignored.
1100     *
1101     * @param margin  the margin percentage (for example, 0.05 is five percent).
1102     *
1103     * @see #getLowerMargin()
1104     * @see #setUpperMargin(double)
1105     */
1106    public void setLowerMargin(double margin) {
1107        this.lowerMargin = margin;
1108        if (isAutoRange()) {
1109            autoAdjustRange();
1110        }
1111        notifyListeners(new AxisChangeEvent(this));
1112    }
1113
1114    /**
1115     * Returns the upper margin for the axis, expressed as a percentage of the
1116     * axis range.  This controls the space added to the lower end of the axis
1117     * when the axis range is automatically calculated (it is ignored when the
1118     * axis range is set explicitly). The default value is 0.05 (five percent).
1119     *
1120     * @return The upper margin.
1121     *
1122     * @see #setUpperMargin(double)
1123     */
1124    public double getUpperMargin() {
1125        return this.upperMargin;
1126    }
1127
1128    /**
1129     * Sets the upper margin for the axis (as a percentage of the axis range)
1130     * and sends an {@link AxisChangeEvent} to all registered listeners.  This
1131     * margin is added only when the axis range is auto-calculated - if you set
1132     * the axis range manually, the margin is ignored.
1133     *
1134     * @param margin  the margin percentage (for example, 0.05 is five percent).
1135     *
1136     * @see #getLowerMargin()
1137     * @see #setLowerMargin(double)
1138     */
1139    public void setUpperMargin(double margin) {
1140        this.upperMargin = margin;
1141        if (isAutoRange()) {
1142            autoAdjustRange();
1143        }
1144        notifyListeners(new AxisChangeEvent(this));
1145    }
1146
1147    /**
1148     * Returns the fixed auto range.
1149     *
1150     * @return The length.
1151     *
1152     * @see #setFixedAutoRange(double)
1153     */
1154    public double getFixedAutoRange() {
1155        return this.fixedAutoRange;
1156    }
1157
1158    /**
1159     * Sets the fixed auto range for the axis.
1160     *
1161     * @param length  the range length.
1162     *
1163     * @see #getFixedAutoRange()
1164     */
1165    public void setFixedAutoRange(double length) {
1166        this.fixedAutoRange = length;
1167        if (isAutoRange()) {
1168            autoAdjustRange();
1169        }
1170        notifyListeners(new AxisChangeEvent(this));
1171    }
1172
1173    /**
1174     * Returns the lower bound of the axis range.
1175     *
1176     * @return The lower bound.
1177     *
1178     * @see #setLowerBound(double)
1179     */
1180    public double getLowerBound() {
1181        return this.range.getLowerBound();
1182    }
1183
1184    /**
1185     * Sets the lower bound for the axis range.  An {@link AxisChangeEvent} is
1186     * sent to all registered listeners.
1187     *
1188     * @param min  the new minimum.
1189     *
1190     * @see #getLowerBound()
1191     */
1192    public void setLowerBound(double min) {
1193        if (this.range.getUpperBound() > min) {
1194            setRange(new Range(min, this.range.getUpperBound()));
1195        }
1196        else {
1197            setRange(new Range(min, min + 1.0));
1198        }
1199    }
1200
1201    /**
1202     * Returns the upper bound for the axis range.
1203     *
1204     * @return The upper bound.
1205     *
1206     * @see #setUpperBound(double)
1207     */
1208    public double getUpperBound() {
1209        return this.range.getUpperBound();
1210    }
1211
1212    /**
1213     * Sets the upper bound for the axis range, and sends an
1214     * {@link AxisChangeEvent} to all registered listeners.
1215     *
1216     * @param max  the new maximum.
1217     *
1218     * @see #getUpperBound()
1219     */
1220    public void setUpperBound(double max) {
1221        if (this.range.getLowerBound() < max) {
1222            setRange(new Range(this.range.getLowerBound(), max));
1223        }
1224        else {
1225            setRange(max - 1.0, max);
1226        }
1227    }
1228
1229    /**
1230     * Returns the range for the axis.
1231     *
1232     * @return The axis range (never <code>null</code>).
1233     *
1234     * @see #setRange(Range)
1235     */
1236    public Range getRange() {
1237        return this.range;
1238    }
1239
1240    /**
1241     * Sets the range attribute and sends an {@link AxisChangeEvent} to all
1242     * registered listeners.  As a side-effect, the auto-range flag is set to
1243     * <code>false</code>.
1244     *
1245     * @param range  the range (<code>null</code> not permitted).
1246     *
1247     * @see #getRange()
1248     */
1249    public void setRange(Range range) {
1250        // defer argument checking
1251        setRange(range, true, true);
1252    }
1253
1254    /**
1255     * Sets the range for the axis, if requested, sends an
1256     * {@link AxisChangeEvent} to all registered listeners.  As a side-effect,
1257     * the auto-range flag is set to <code>false</code> (optional).
1258     *
1259     * @param range  the range (<code>null</code> not permitted).
1260     * @param turnOffAutoRange  a flag that controls whether or not the auto
1261     *                          range is turned off.
1262     * @param notify  a flag that controls whether or not listeners are
1263     *                notified.
1264     *
1265     * @see #getRange()
1266     */
1267    public void setRange(Range range, boolean turnOffAutoRange,
1268                         boolean notify) {
1269        if (range == null) {
1270            throw new IllegalArgumentException("Null 'range' argument.");
1271        }
1272        if (turnOffAutoRange) {
1273            this.autoRange = false;
1274        }
1275        this.range = range;
1276        if (notify) {
1277            notifyListeners(new AxisChangeEvent(this));
1278        }
1279    }
1280
1281    /**
1282     * Sets the axis range and sends an {@link AxisChangeEvent} to all
1283     * registered listeners.  As a side-effect, the auto-range flag is set to
1284     * <code>false</code>.
1285     *
1286     * @param lower  the lower axis limit.
1287     * @param upper  the upper axis limit.
1288     *
1289     * @see #getRange()
1290     * @see #setRange(Range)
1291     */
1292    public void setRange(double lower, double upper) {
1293        setRange(new Range(lower, upper));
1294    }
1295
1296    /**
1297     * Sets the range for the axis (after first adding the current margins to
1298     * the specified range) and sends an {@link AxisChangeEvent} to all
1299     * registered listeners.
1300     *
1301     * @param range  the range (<code>null</code> not permitted).
1302     */
1303    public void setRangeWithMargins(Range range) {
1304        setRangeWithMargins(range, true, true);
1305    }
1306
1307    /**
1308     * Sets the range for the axis after first adding the current margins to
1309     * the range and, if requested, sends an {@link AxisChangeEvent} to all
1310     * registered listeners.  As a side-effect, the auto-range flag is set to
1311     * <code>false</code> (optional).
1312     *
1313     * @param range  the range (excluding margins, <code>null</code> not
1314     *               permitted).
1315     * @param turnOffAutoRange  a flag that controls whether or not the auto
1316     *                          range is turned off.
1317     * @param notify  a flag that controls whether or not listeners are
1318     *                notified.
1319     */
1320    public void setRangeWithMargins(Range range, boolean turnOffAutoRange,
1321                                    boolean notify) {
1322        if (range == null) {
1323            throw new IllegalArgumentException("Null 'range' argument.");
1324        }
1325        setRange(Range.expand(range, getLowerMargin(), getUpperMargin()),
1326                turnOffAutoRange, notify);
1327    }
1328
1329    /**
1330     * Sets the axis range (after first adding the current margins to the
1331     * range) and sends an {@link AxisChangeEvent} to all registered listeners.
1332     * As a side-effect, the auto-range flag is set to <code>false</code>.
1333     *
1334     * @param lower  the lower axis limit.
1335     * @param upper  the upper axis limit.
1336     */
1337    public void setRangeWithMargins(double lower, double upper) {
1338        setRangeWithMargins(new Range(lower, upper));
1339    }
1340
1341    /**
1342     * Sets the axis range, where the new range is 'size' in length, and
1343     * centered on 'value'.
1344     *
1345     * @param value  the central value.
1346     * @param length  the range length.
1347     */
1348    public void setRangeAboutValue(double value, double length) {
1349        setRange(new Range(value - length / 2, value + length / 2));
1350    }
1351
1352    /**
1353     * Returns a flag indicating whether or not the tick unit is automatically
1354     * selected from a range of standard tick units.
1355     *
1356     * @return A flag indicating whether or not the tick unit is automatically
1357     *         selected.
1358     *
1359     * @see #setAutoTickUnitSelection(boolean)
1360     */
1361    public boolean isAutoTickUnitSelection() {
1362        return this.autoTickUnitSelection;
1363    }
1364
1365    /**
1366     * Sets a flag indicating whether or not the tick unit is automatically
1367     * selected from a range of standard tick units.  If the flag is changed,
1368     * registered listeners are notified that the chart has changed.
1369     *
1370     * @param flag  the new value of the flag.
1371     *
1372     * @see #isAutoTickUnitSelection()
1373     */
1374    public void setAutoTickUnitSelection(boolean flag) {
1375        setAutoTickUnitSelection(flag, true);
1376    }
1377
1378    /**
1379     * Sets a flag indicating whether or not the tick unit is automatically
1380     * selected from a range of standard tick units.
1381     *
1382     * @param flag  the new value of the flag.
1383     * @param notify  notify listeners?
1384     *
1385     * @see #isAutoTickUnitSelection()
1386     */
1387    public void setAutoTickUnitSelection(boolean flag, boolean notify) {
1388
1389        if (this.autoTickUnitSelection != flag) {
1390            this.autoTickUnitSelection = flag;
1391            if (notify) {
1392                notifyListeners(new AxisChangeEvent(this));
1393            }
1394        }
1395    }
1396
1397    /**
1398     * Returns the source for obtaining standard tick units for the axis.
1399     *
1400     * @return The source (possibly <code>null</code>).
1401     *
1402     * @see #setStandardTickUnits(TickUnitSource)
1403     */
1404    public TickUnitSource getStandardTickUnits() {
1405        return this.standardTickUnits;
1406    }
1407
1408    /**
1409     * Sets the source for obtaining standard tick units for the axis and sends
1410     * an {@link AxisChangeEvent} to all registered listeners.  The axis will
1411     * try to select the smallest tick unit from the source that does not cause
1412     * the tick labels to overlap (see also the
1413     * {@link #setAutoTickUnitSelection(boolean)} method.
1414     *
1415     * @param source  the source for standard tick units (<code>null</code>
1416     *                permitted).
1417     *
1418     * @see #getStandardTickUnits()
1419     */
1420    public void setStandardTickUnits(TickUnitSource source) {
1421        this.standardTickUnits = source;
1422        notifyListeners(new AxisChangeEvent(this));
1423    }
1424
1425    /**
1426     * Returns the number of minor tick marks to display.
1427     *
1428     * @return The number of minor tick marks to display.
1429     *
1430     * @see #setMinorTickCount(int)
1431     *
1432     * @since 1.0.12
1433     */
1434    public int getMinorTickCount() {
1435        return this.minorTickCount;
1436    }
1437
1438    /**
1439     * Sets the number of minor tick marks to display, and sends an
1440     * {@link AxisChangeEvent} to all registered listeners.
1441     *
1442     * @param count  the count.
1443     *
1444     * @see #getMinorTickCount()
1445     *
1446     * @since 1.0.12
1447     */
1448    public void setMinorTickCount(int count) {
1449        this.minorTickCount = count;
1450        notifyListeners(new AxisChangeEvent(this));
1451    }
1452
1453    /**
1454     * Converts a data value to a coordinate in Java2D space, assuming that the
1455     * axis runs along one edge of the specified dataArea.
1456     * <p>
1457     * Note that it is possible for the coordinate to fall outside the area.
1458     *
1459     * @param value  the data value.
1460     * @param area  the area for plotting the data.
1461     * @param edge  the edge along which the axis lies.
1462     *
1463     * @return The Java2D coordinate.
1464     *
1465     * @see #java2DToValue(double, Rectangle2D, RectangleEdge)
1466     */
1467    public abstract double valueToJava2D(double value, Rectangle2D area,
1468                                         RectangleEdge edge);
1469
1470    /**
1471     * Converts a length in data coordinates into the corresponding length in
1472     * Java2D coordinates.
1473     *
1474     * @param length  the length.
1475     * @param area  the plot area.
1476     * @param edge  the edge along which the axis lies.
1477     *
1478     * @return The length in Java2D coordinates.
1479     */
1480    public double lengthToJava2D(double length, Rectangle2D area,
1481                                 RectangleEdge edge) {
1482        double zero = valueToJava2D(0.0, area, edge);
1483        double l = valueToJava2D(length, area, edge);
1484        return Math.abs(l - zero);
1485    }
1486
1487    /**
1488     * Converts a coordinate in Java2D space to the corresponding data value,
1489     * assuming that the axis runs along one edge of the specified dataArea.
1490     *
1491     * @param java2DValue  the coordinate in Java2D space.
1492     * @param area  the area in which the data is plotted.
1493     * @param edge  the edge along which the axis lies.
1494     *
1495     * @return The data value.
1496     *
1497     * @see #valueToJava2D(double, Rectangle2D, RectangleEdge)
1498     */
1499    public abstract double java2DToValue(double java2DValue,
1500                                         Rectangle2D area,
1501                                         RectangleEdge edge);
1502
1503    /**
1504     * Automatically sets the axis range to fit the range of values in the
1505     * dataset.  Sometimes this can depend on the renderer used as well (for
1506     * example, the renderer may "stack" values, requiring an axis range
1507     * greater than otherwise necessary).
1508     */
1509    protected abstract void autoAdjustRange();
1510
1511    /**
1512     * Centers the axis range about the specified value and sends an
1513     * {@link AxisChangeEvent} to all registered listeners.
1514     *
1515     * @param value  the center value.
1516     */
1517    public void centerRange(double value) {
1518
1519        double central = this.range.getCentralValue();
1520        Range adjusted = new Range(this.range.getLowerBound() + value - central,
1521                this.range.getUpperBound() + value - central);
1522        setRange(adjusted);
1523
1524    }
1525
1526    /**
1527     * Increases or decreases the axis range by the specified percentage about
1528     * the central value and sends an {@link AxisChangeEvent} to all registered
1529     * listeners.
1530     * <P>
1531     * To double the length of the axis range, use 200% (2.0).
1532     * To halve the length of the axis range, use 50% (0.5).
1533     *
1534     * @param percent  the resize factor.
1535     *
1536     * @see #resizeRange(double, double)
1537     */
1538    public void resizeRange(double percent) {
1539        resizeRange(percent, this.range.getCentralValue());
1540    }
1541
1542    /**
1543     * Increases or decreases the axis range by the specified percentage about
1544     * the specified anchor value and sends an {@link AxisChangeEvent} to all
1545     * registered listeners.
1546     * <P>
1547     * To double the length of the axis range, use 200% (2.0).
1548     * To halve the length of the axis range, use 50% (0.5).
1549     *
1550     * @param percent  the resize factor.
1551     * @param anchorValue  the new central value after the resize.
1552     *
1553     * @see #resizeRange(double)
1554     */
1555    public void resizeRange(double percent, double anchorValue) {
1556        if (percent > 0.0) {
1557            double halfLength = this.range.getLength() * percent / 2;
1558            Range adjusted = new Range(anchorValue - halfLength,
1559                    anchorValue + halfLength);
1560            setRange(adjusted);
1561        }
1562        else {
1563            setAutoRange(true);
1564        }
1565    }
1566
1567    /**
1568     * Increases or decreases the axis range by the specified percentage about
1569     * the specified anchor value and sends an {@link AxisChangeEvent} to all
1570     * registered listeners.
1571     * <P>
1572     * To double the length of the axis range, use 200% (2.0).
1573     * To halve the length of the axis range, use 50% (0.5).
1574     *
1575     * @param percent  the resize factor.
1576     * @param anchorValue  the new central value after the resize.
1577     *
1578     * @see #resizeRange(double)
1579     *
1580     * @since 1.0.13
1581     */
1582    public void resizeRange2(double percent, double anchorValue) {
1583        if (percent > 0.0) {
1584            double left = anchorValue - getLowerBound();
1585            double right = getUpperBound() - anchorValue;
1586            Range adjusted = new Range(anchorValue - left * percent,
1587                    anchorValue + right * percent);
1588            setRange(adjusted);
1589        }
1590        else {
1591            setAutoRange(true);
1592        }
1593    }
1594
1595    /**
1596     * Zooms in on the current range.
1597     *
1598     * @param lowerPercent  the new lower bound.
1599     * @param upperPercent  the new upper bound.
1600     */
1601    public void zoomRange(double lowerPercent, double upperPercent) {
1602        double start = this.range.getLowerBound();
1603        double length = this.range.getLength();
1604        Range adjusted = null;
1605        if (isInverted()) {
1606            adjusted = new Range(start + (length * (1 - upperPercent)),
1607                                 start + (length * (1 - lowerPercent)));
1608        }
1609        else {
1610            adjusted = new Range(start + length * lowerPercent,
1611                    start + length * upperPercent);
1612        }
1613        setRange(adjusted);
1614    }
1615
1616    /**
1617     * Slides the axis range by the specified percentage.
1618     *
1619     * @param percent  the percentage.
1620     *
1621     * @since 1.0.13
1622     */
1623    public void pan(double percent) {
1624        Range range = getRange();
1625        double length = range.getLength();
1626        double adj = length * percent;
1627        double lower = range.getLowerBound() + adj;
1628        double upper = range.getUpperBound() + adj;
1629        setRange(lower, upper);
1630    }
1631
1632    /**
1633     * Returns the auto tick index.
1634     *
1635     * @return The auto tick index.
1636     *
1637     * @see #setAutoTickIndex(int)
1638     */
1639    protected int getAutoTickIndex() {
1640        return this.autoTickIndex;
1641    }
1642
1643    /**
1644     * Sets the auto tick index.
1645     *
1646     * @param index  the new value.
1647     *
1648     * @see #getAutoTickIndex()
1649     */
1650    protected void setAutoTickIndex(int index) {
1651        this.autoTickIndex = index;
1652    }
1653
1654    /**
1655     * Tests the axis for equality with an arbitrary object.
1656     *
1657     * @param obj  the object (<code>null</code> permitted).
1658     *
1659     * @return <code>true</code> or <code>false</code>.
1660     */
1661    public boolean equals(Object obj) {
1662        if (obj == this) {
1663            return true;
1664        }
1665        if (!(obj instanceof ValueAxis)) {
1666            return false;
1667        }
1668        ValueAxis that = (ValueAxis) obj;
1669        if (this.positiveArrowVisible != that.positiveArrowVisible) {
1670            return false;
1671        }
1672        if (this.negativeArrowVisible != that.negativeArrowVisible) {
1673            return false;
1674        }
1675        if (this.inverted != that.inverted) {
1676            return false;
1677        }
1678        // if autoRange is true, then the current range is irrelevant
1679        if (!this.autoRange && !ObjectUtilities.equal(this.range, that.range)) {
1680            return false;
1681        }
1682        if (this.autoRange != that.autoRange) {
1683            return false;
1684        }
1685        if (this.autoRangeMinimumSize != that.autoRangeMinimumSize) {
1686            return false;
1687        }
1688        if (!this.defaultAutoRange.equals(that.defaultAutoRange)) {
1689            return false;
1690        }
1691        if (this.upperMargin != that.upperMargin) {
1692            return false;
1693        }
1694        if (this.lowerMargin != that.lowerMargin) {
1695            return false;
1696        }
1697        if (this.fixedAutoRange != that.fixedAutoRange) {
1698            return false;
1699        }
1700        if (this.autoTickUnitSelection != that.autoTickUnitSelection) {
1701            return false;
1702        }
1703        if (!ObjectUtilities.equal(this.standardTickUnits,
1704                that.standardTickUnits)) {
1705            return false;
1706        }
1707        if (this.verticalTickLabels != that.verticalTickLabels) {
1708            return false;
1709        }
1710        if (this.minorTickCount != that.minorTickCount) {
1711            return false;
1712        }
1713        return super.equals(obj);
1714    }
1715
1716    /**
1717     * Returns a clone of the object.
1718     *
1719     * @return A clone.
1720     *
1721     * @throws CloneNotSupportedException if some component of the axis does
1722     *         not support cloning.
1723     */
1724    public Object clone() throws CloneNotSupportedException {
1725        ValueAxis clone = (ValueAxis) super.clone();
1726        return clone;
1727    }
1728
1729    /**
1730     * Provides serialization support.
1731     *
1732     * @param stream  the output stream.
1733     *
1734     * @throws IOException  if there is an I/O error.
1735     */
1736    private void writeObject(ObjectOutputStream stream) throws IOException {
1737        stream.defaultWriteObject();
1738        SerialUtilities.writeShape(this.upArrow, stream);
1739        SerialUtilities.writeShape(this.downArrow, stream);
1740        SerialUtilities.writeShape(this.leftArrow, stream);
1741        SerialUtilities.writeShape(this.rightArrow, stream);
1742    }
1743
1744    /**
1745     * Provides serialization support.
1746     *
1747     * @param stream  the input stream.
1748     *
1749     * @throws IOException  if there is an I/O error.
1750     * @throws ClassNotFoundException  if there is a classpath problem.
1751     */
1752    private void readObject(ObjectInputStream stream)
1753            throws IOException, ClassNotFoundException {
1754
1755        stream.defaultReadObject();
1756        this.upArrow = SerialUtilities.readShape(stream);
1757        this.downArrow = SerialUtilities.readShape(stream);
1758        this.leftArrow = SerialUtilities.readShape(stream);
1759        this.rightArrow = SerialUtilities.readShape(stream);
1760    }
1761
1762}