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