content: /* ===========================================================
   * JFreeChart : a free chart library for the Java(tm) platform
   * ===========================================================
   *
   * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
   *
   * Project Info:  http://www.jfree.org/jfreechart/index.html
   *
   * This library is free software; you can redistribute it and/or modify it
   * under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation; either version 2.1 of the License, or
   * (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful, but
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
   * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
   * License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
   * USA.
   *
   * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
   * Other names may be trademarks of their respective owners.]
   *
   * --------------
   * ValueAxis.java
   * --------------
   * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
   *
   * Original Author:  David Gilbert (for Object Refinery Limited);
   * Contributor(s):   Jonathan Nash;
   *                   Nicolas Brodu (for Astrium and EADS Corporate Research
   *                   Center);
   *                   Peter Kolb (patch 1934255);
   *                   Andrew Mickish (patch 1870189);
   *
   * Changes
   * -------
   * 18-Sep-2001 : Added standard header and fixed DOS encoding problem (DG);
   * 23-Nov-2001 : Overhauled standard tick unit code (DG);
   * 04-Dec-2001 : Changed constructors to protected, and tidied up default
   *               values (DG);
   * 12-Dec-2001 : Fixed vertical gridlines bug (DG);
   * 16-Jan-2002 : Added an optional crosshair, based on the implementation by
   *               Jonathan Nash (DG);
   * 23-Jan-2002 : Moved the minimum and maximum values to here from NumberAxis,
   *               and changed the type from Number to double (DG);
   * 25-Feb-2002 : Added default value for autoRange. Changed autoAdjustRange
   *               from public to protected. Updated import statements (DG);
   * 23-Apr-2002 : Added setRange() method (DG);
   * 29-Apr-2002 : Added range adjustment methods (DG);
   * 13-Jun-2002 : Modified setCrosshairValue() to notify listeners only when the
   *               crosshairs are visible, to avoid unnecessary repaints, as
   *               suggested by Kees Kuip (DG);
   * 25-Jul-2002 : Moved lower and upper margin attributes from the NumberAxis
   *               class (DG);
   * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);
   * 01-Oct-2002 : Fixed errors reported by Checkstyle (DG);
   * 04-Oct-2002 : Moved standardTickUnits from NumberAxis --> ValueAxis (DG);
   * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
   * 19-Nov-2002 : Removed grid settings (now controlled by the plot) (DG);
   * 27-Nov-2002 : Moved the 'inverted' attribute from NumberAxis to
   *               ValueAxis (DG);
   * 03-Jan-2003 : Small fix to ensure auto-range minimum is observed
   *               immediately (DG);
   * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double (DG);
   * 20-Jan-2003 : Replaced monolithic constructor (DG);
   * 26-Mar-2003 : Implemented Serializable (DG);
   * 09-May-2003 : Added AxisLocation parameter to translation methods (DG);
   * 13-Aug-2003 : Implemented Cloneable (DG);
   * 01-Sep-2003 : Fixed bug 793167 (setMaximumAxisValue exception) (DG);
   * 02-Sep-2003 : Fixed bug 795366 (zooming on inverted axes) (DG);
   * 08-Sep-2003 : Completed Serialization support (NB);
   * 08-Sep-2003 : Renamed get/setMinimumValue --> get/setLowerBound,
   *               and get/setMaximumValue --> get/setUpperBound (DG);
   * 27-Oct-2003 : Changed DEFAULT_AUTO_RANGE_MINIMUM_SIZE value - see bug ID
   *               829606 (DG);
   * 07-Nov-2003 : Changes to tick mechanism (DG);
   * 06-Jan-2004 : Moved axis line attributes to Axis class (DG);
   * 21-Jan-2004 : Removed redundant axisLineVisible attribute.  Renamed
   *               translateJava2DToValue --> java2DToValue, and
   *               translateValueToJava2D --> valueToJava2D (DG);
   * 23-Jan-2004 : Fixed setAxisLinePaint() and setAxisLineStroke() which had no
   *               effect (andreas.gawecki@coremedia.com);
   * 07-Apr-2004 : Changed text bounds calculation (DG);
   * 26-Apr-2004 : Added getter/setter methods for arrow shapes (DG);
   * 18-May-2004 : Added methods to set axis range *including* current
   *               margins (DG);
   * 02-Jun-2004 : Fixed bug in setRangeWithMargins() method (DG);
   * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities
   *               --> TextUtilities (DG);
   * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
   *               release (DG);
   * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
   * ------------- JFREECHART 1.0.x ---------------------------------------------
   * 10-Oct-2006 : Source reformatting (DG);
   * 22-Mar-2007 : Added new defaultAutoRange attribute (DG);
   * 02-Aug-2007 : Check for major tick when drawing label (DG);
   * 25-Sep-2008 : Added minor tick support, see patch 1934255 by Peter Kolb (DG);
   * 21-Jan-2009 : Updated default behaviour of minor ticks (DG);
   * 18-Mar-2008 : Added resizeRange2() method which provides more natural
   *               anchored zooming for mouse wheel support (DG);
   * 26-Mar-2009 : In equals(), only check current range if autoRange is
   *               false (DG);
   * 30-Mar-2009 : Added pan(double) method (DG);
   * 15-Jun-2012 : Remove JCommon dependencies (DG);
   *
   */
  
  package org.jfree.chart.axis;
  
  import java.awt.Font;
  import java.awt.FontMetrics;
  import java.awt.Graphics2D;
  import java.awt.Polygon;
  import java.awt.Shape;
  import java.awt.font.LineMetrics;
  import java.awt.geom.AffineTransform;
  import java.awt.geom.Line2D;
  import java.awt.geom.Rectangle2D;
  import java.io.IOException;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutputStream;
  import java.io.Serializable;
  import java.util.List;
  
  import org.jfree.chart.ui.RectangleEdge;
  import org.jfree.chart.ui.RectangleInsets;
  import org.jfree.chart.util.ObjectUtilities;
  import org.jfree.chart.util.PublicCloneable;
  import org.jfree.chart.event.AxisChangeEvent;
  import org.jfree.chart.plot.Plot;
  import org.jfree.chart.text.TextUtilities;
  import org.jfree.chart.util.ParamChecks;
  import org.jfree.chart.util.SerialUtilities;
  import org.jfree.data.Range;
  
  /**
   * The base class for axes that display value data, where values are measured
   * using the <code>double</code> primitive.  The two key subclasses are
   * {@link DateAxis} and {@link NumberAxis}.
   */
  public abstract class ValueAxis extends Axis
          implements Cloneable, PublicCloneable, Serializable {
  
      /** For serialization. */
      private static final long serialVersionUID = 3698345477322391456L;
  
      /** The default axis range. */
      public static final Range DEFAULT_RANGE = new Range(0.0, 1.0);
  
      /** The default auto-range value. */
      public static final boolean DEFAULT_AUTO_RANGE = true;
  
      /** The default inverted flag setting. */
      public static final boolean DEFAULT_INVERTED = false;
  
      /** The default minimum auto range. */
      public static final double DEFAULT_AUTO_RANGE_MINIMUM_SIZE = 0.00000001;
  
      /** The default value for the lower margin (0.05 = 5%). */
      public static final double DEFAULT_LOWER_MARGIN = 0.05;
  
      /** The default value for the upper margin (0.05 = 5%). */
      public static final double DEFAULT_UPPER_MARGIN = 0.05;
  
      /** The default auto-tick-unit-selection value. */
      public static final boolean DEFAULT_AUTO_TICK_UNIT_SELECTION = true;
  
      /** The maximum tick count. */
      public static final int MAXIMUM_TICK_COUNT = 500;
  
      /**
       * A flag that controls whether an arrow is drawn at the positive end of
       * the axis line.
       */
      private boolean positiveArrowVisible;
  
      /**
       * A flag that controls whether an arrow is drawn at the negative end of
       * the axis line.
       */
      private boolean negativeArrowVisible;
  
      /** The shape used for an up arrow. */
      private transient Shape upArrow;
  
      /** The shape used for a down arrow. */
      private transient Shape downArrow;
  
      /** The shape used for a left arrow. */
      private transient Shape leftArrow;
  
      /** The shape used for a right arrow. */
      private transient Shape rightArrow;
  
      /** A flag that affects the orientation of the values on the axis. */
      private boolean inverted;
  
      /** The axis range. */
      private Range range;
  
      /**
       * Flag that indicates whether the axis automatically scales to fit the
       * chart data.
       */
      private boolean autoRange;
  
      /** The minimum size for the 'auto' axis range (excluding margins). */
      private double autoRangeMinimumSize;
  
      /**
       * The default range is used when the dataset is empty and the axis needs
       * to determine the auto range.
       *
       * @since 1.0.5
       */
      private Range defaultAutoRange;
  
      /**
       * The upper margin percentage.  This indicates the amount by which the
       * maximum axis value exceeds the maximum data value (as a percentage of
       * the range on the axis) when the axis range is determined automatically.
       */
      private double upperMargin;
  
      /**
       * The lower margin.  This is a percentage that indicates the amount by
       * which the minimum axis value is "less than" the minimum data value when
       * the axis range is determined automatically.
       */
      private double lowerMargin;
  
      /**
       * If this value is positive, the amount is subtracted from the maximum
       * data value to determine the lower axis range.  This can be used to
       * provide a fixed "window" on dynamic data.
       */
      private double fixedAutoRange;
  
      /**
       * Flag that indicates whether or not the tick unit is selected
       * automatically.
       */
      private boolean autoTickUnitSelection;
  
      /** The standard tick units for the axis. */
      private TickUnitSource standardTickUnits;
  
      /** An index into an array of standard tick values. */
      private int autoTickIndex;
  
      /**
       * The number of minor ticks per major tick unit.  This is an override
       * field, if the value is > 0 it is used, otherwise the axis refers to the
       * minorTickCount in the current tickUnit.
       */
      private int minorTickCount;
  
      /** A flag indicating whether or not tick labels are rotated to vertical. */
      private boolean verticalTickLabels;
  
      /**
       * Constructs a value axis.
       *
       * @param label  the axis label (<code>null</code> permitted).
       * @param standardTickUnits  the source for standard tick units
       *                           (<code>null</code> permitted).
       */
      protected ValueAxis(String label, TickUnitSource standardTickUnits) {
  
          super(label);
  
          this.positiveArrowVisible = false;
          this.negativeArrowVisible = false;
  
          this.range = DEFAULT_RANGE;
          this.autoRange = DEFAULT_AUTO_RANGE;
          this.defaultAutoRange = DEFAULT_RANGE;
  
          this.inverted = DEFAULT_INVERTED;
          this.autoRangeMinimumSize = DEFAULT_AUTO_RANGE_MINIMUM_SIZE;
  
          this.lowerMargin = DEFAULT_LOWER_MARGIN;
          this.upperMargin = DEFAULT_UPPER_MARGIN;
  
          this.fixedAutoRange = 0.0;
  
          this.autoTickUnitSelection = DEFAULT_AUTO_TICK_UNIT_SELECTION;
          this.standardTickUnits = standardTickUnits;
  
          Polygon p1 = new Polygon();
          p1.addPoint(0, 0);
          p1.addPoint(-2, 2);
          p1.addPoint(2, 2);
  
          this.upArrow = p1;
  
          Polygon p2 = new Polygon();
          p2.addPoint(0, 0);
          p2.addPoint(-2, -2);
          p2.addPoint(2, -2);
  
          this.downArrow = p2;
  
          Polygon p3 = new Polygon();
          p3.addPoint(0, 0);
          p3.addPoint(-2, -2);
          p3.addPoint(-2, 2);
  
          this.rightArrow = p3;
  
          Polygon p4 = new Polygon();
          p4.addPoint(0, 0);
          p4.addPoint(2, -2);
          p4.addPoint(2, 2);
  
          this.leftArrow = p4;
  
          this.verticalTickLabels = false;
          this.minorTickCount = 0;
  
      }
  
      /**
       * Returns <code>true</code> if the tick labels should be rotated (to
       * vertical), and <code>false</code> otherwise.
       *
       * @return <code>true</code> or <code>false</code>.
       *
       * @see #setVerticalTickLabels(boolean)
       */
      public boolean isVerticalTickLabels() {
          return this.verticalTickLabels;
      }
  
      /**
       * Sets the flag that controls whether the tick labels are displayed
       * vertically (that is, rotated 90 degrees from horizontal).  If the flag
       * is changed, an {@link AxisChangeEvent} is sent to all registered
       * listeners.
       *
       * @param flag  the flag.
       *
       * @see #isVerticalTickLabels()
       */
      public void setVerticalTickLabels(boolean flag) {
          if (this.verticalTickLabels != flag) {
              this.verticalTickLabels = flag;
              fireChangeEvent();
          }
      }
  
      /**
       * Returns a flag that controls whether or not the axis line has an arrow
       * drawn that points in the positive direction for the axis.
       *
       * @return A boolean.
       *
       * @see #setPositiveArrowVisible(boolean)
       */
      public boolean isPositiveArrowVisible() {
          return this.positiveArrowVisible;
      }
  
      /**
       * Sets a flag that controls whether or not the axis lines has an arrow
       * drawn that points in the positive direction for the axis, and sends an
       * {@link AxisChangeEvent} to all registered listeners.
       *
       * @param visible  the flag.
       *
       * @see #isPositiveArrowVisible()
       */
      public void setPositiveArrowVisible(boolean visible) {
          this.positiveArrowVisible = visible;
          fireChangeEvent();
      }
  
      /**
       * Returns a flag that controls whether or not the axis line has an arrow
       * drawn that points in the negative direction for the axis.
       *
       * @return A boolean.
       *
       * @see #setNegativeArrowVisible(boolean)
       */
      public boolean isNegativeArrowVisible() {
          return this.negativeArrowVisible;
      }
  
      /**
       * Sets a flag that controls whether or not the axis lines has an arrow
       * drawn that points in the negative direction for the axis, and sends an
       * {@link AxisChangeEvent} to all registered listeners.
       *
       * @param visible  the flag.
       *
       * @see #setNegativeArrowVisible(boolean)
       */
      public void setNegativeArrowVisible(boolean visible) {
          this.negativeArrowVisible = visible;
          fireChangeEvent();
      }
  
      /**
       * Returns a shape that can be displayed as an arrow pointing upwards at
       * the end of an axis line.
       *
       * @return A shape (never <code>null</code>).
       *
       * @see #setUpArrow(Shape)
       */
      public Shape getUpArrow() {
          return this.upArrow;
      }
  
      /**
       * Sets the shape that can be displayed as an arrow pointing upwards at
       * the end of an axis line and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       *
       * @param arrow  the arrow shape (<code>null</code> not permitted).
       *
       * @see #getUpArrow()
       */
      public void setUpArrow(Shape arrow) {
          ParamChecks.nullNotPermitted(arrow, "arrow");
          this.upArrow = arrow;
          fireChangeEvent();
      }
  
      /**
       * Returns a shape that can be displayed as an arrow pointing downwards at
       * the end of an axis line.
       *
       * @return A shape (never <code>null</code>).
       *
       * @see #setDownArrow(Shape)
       */
      public Shape getDownArrow() {
          return this.downArrow;
      }
  
      /**
       * Sets the shape that can be displayed as an arrow pointing downwards at
       * the end of an axis line and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       *
       * @param arrow  the arrow shape (<code>null</code> not permitted).
       *
       * @see #getDownArrow()
       */
      public void setDownArrow(Shape arrow) {
          ParamChecks.nullNotPermitted(arrow, "arrow");
          this.downArrow = arrow;
          fireChangeEvent();
      }
  
      /**
       * Returns a shape that can be displayed as an arrow pointing left at the
       * end of an axis line.
       *
       * @return A shape (never <code>null</code>).
       *
       * @see #setLeftArrow(Shape)
       */
      public Shape getLeftArrow() {
          return this.leftArrow;
      }
  
      /**
       * Sets the shape that can be displayed as an arrow pointing left at the
       * end of an axis line and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       *
       * @param arrow  the arrow shape (<code>null</code> not permitted).
       *
       * @see #getLeftArrow()
       */
      public void setLeftArrow(Shape arrow) {
          ParamChecks.nullNotPermitted(arrow, "arrow");
          this.leftArrow = arrow;
          fireChangeEvent();
      }
  
      /**
       * Returns a shape that can be displayed as an arrow pointing right at the
       * end of an axis line.
       *
       * @return A shape (never <code>null</code>).
       *
       * @see #setRightArrow(Shape)
       */
      public Shape getRightArrow() {
          return this.rightArrow;
      }
  
      /**
       * Sets the shape that can be displayed as an arrow pointing rightwards at
       * the end of an axis line and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       *
       * @param arrow  the arrow shape (<code>null</code> not permitted).
       *
       * @see #getRightArrow()
       */
      public void setRightArrow(Shape arrow) {
          ParamChecks.nullNotPermitted(arrow, "arrow");
          this.rightArrow = arrow;
          fireChangeEvent();
      }
  
      /**
       * Draws an axis line at the current cursor position and edge.
       *
       * @param g2  the graphics device.
       * @param cursor  the cursor position.
       * @param dataArea  the data area.
       * @param edge  the edge.
       */
      @Override
      protected void drawAxisLine(Graphics2D g2, double cursor,
                                  Rectangle2D dataArea, RectangleEdge edge) {
          Line2D axisLine = null;
          if (edge == RectangleEdge.TOP) {
              axisLine = new Line2D.Double(dataArea.getX(), cursor,
                      dataArea.getMaxX(), cursor);
          }
          else if (edge == RectangleEdge.BOTTOM) {
              axisLine = new Line2D.Double(dataArea.getX(), cursor,
                      dataArea.getMaxX(), cursor);
          }
          else if (edge == RectangleEdge.LEFT) {
              axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
                      dataArea.getMaxY());
          }
          else if (edge == RectangleEdge.RIGHT) {
              axisLine = new Line2D.Double(cursor, dataArea.getY(), cursor,
                      dataArea.getMaxY());
          }
          g2.setPaint(getAxisLinePaint());
          g2.setStroke(getAxisLineStroke());
          g2.draw(axisLine);
  
          boolean drawUpOrRight = false;
          boolean drawDownOrLeft = false;
          if (this.positiveArrowVisible) {
              if (this.inverted) {
                  drawDownOrLeft = true;
              }
              else {
                  drawUpOrRight = true;
              }
          }
          if (this.negativeArrowVisible) {
              if (this.inverted) {
                  drawUpOrRight = true;
              }
              else {
                  drawDownOrLeft = true;
              }
          }
          if (drawUpOrRight) {
              double x = 0.0;
              double y = 0.0;
              Shape arrow = null;
              if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
                  x = dataArea.getMaxX();
                  y = cursor;
                  arrow = this.rightArrow;
              }
              else if (edge == RectangleEdge.LEFT
                      || edge == RectangleEdge.RIGHT) {
                  x = cursor;
                  y = dataArea.getMinY();
                  arrow = this.upArrow;
              }
  
              // draw the arrow...
              AffineTransform transformer = new AffineTransform();
              transformer.setToTranslation(x, y);
              Shape shape = transformer.createTransformedShape(arrow);
              g2.fill(shape);
              g2.draw(shape);
          }
  
          if (drawDownOrLeft) {
              double x = 0.0;
              double y = 0.0;
              Shape arrow = null;
              if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
                  x = dataArea.getMinX();
                  y = cursor;
                  arrow = this.leftArrow;
              }
              else if (edge == RectangleEdge.LEFT
                      || edge == RectangleEdge.RIGHT) {
                  x = cursor;
                  y = dataArea.getMaxY();
                  arrow = this.downArrow;
              }
  
              // draw the arrow...
              AffineTransform transformer = new AffineTransform();
              transformer.setToTranslation(x, y);
              Shape shape = transformer.createTransformedShape(arrow);
              g2.fill(shape);
              g2.draw(shape);
          }
  
      }
  
      /**
       * Calculates the anchor point for a tick label.
       *
       * @param tick  the tick.
       * @param cursor  the cursor.
       * @param dataArea  the data area.
       * @param edge  the edge on which the axis is drawn.
       *
       * @return The x and y coordinates of the anchor point.
       */
      protected float[] calculateAnchorPoint(ValueTick tick, double cursor,
              Rectangle2D dataArea, RectangleEdge edge) {
  
          RectangleInsets insets = getTickLabelInsets();
          float[] result = new float[2];
          if (edge == RectangleEdge.TOP) {
              result[0] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
              result[1] = (float) (cursor - insets.getBottom() - 2.0);
          }
          else if (edge == RectangleEdge.BOTTOM) {
              result[0] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
              result[1] = (float) (cursor + insets.getTop() + 2.0);
          }
          else if (edge == RectangleEdge.LEFT) {
              result[0] = (float) (cursor - insets.getLeft() - 2.0);
              result[1] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
          }
          else if (edge == RectangleEdge.RIGHT) {
              result[0] = (float) (cursor + insets.getRight() + 2.0);
              result[1] = (float) valueToJava2D(tick.getValue(), dataArea, edge);
          }
          return result;
      }
  
      /**
       * Draws the axis line, tick marks and tick mark labels.
       *
       * @param g2  the graphics device.
       * @param cursor  the cursor.
       * @param plotArea  the plot area.
       * @param dataArea  the data area.
       * @param edge  the edge that the axis is aligned with.
       *
       * @return The width or height used to draw the axis.
       */
      protected AxisState drawTickMarksAndLabels(Graphics2D g2,
              double cursor, Rectangle2D plotArea, Rectangle2D dataArea,
              RectangleEdge edge) {
  
          AxisState state = new AxisState(cursor);
  
          if (isAxisLineVisible()) {
              drawAxisLine(g2, cursor, dataArea, edge);
          }
  
          List<ValueTick> ticks = refreshTicks(g2, state, dataArea, edge);
          state.setTicks(ticks);
          g2.setFont(getTickLabelFont());
          for (ValueTick tick : ticks) {
              if (isTickLabelsVisible()) {
                  g2.setPaint(getTickLabelPaint());
                  float[] anchorPoint = calculateAnchorPoint(tick, cursor,
                          dataArea, edge);
                  TextUtilities.drawRotatedString(tick.getText(), g2,
                          anchorPoint[0], anchorPoint[1], tick.getTextAnchor(),
                          tick.getAngle(), tick.getRotationAnchor());
              }
  
              if ((isTickMarksVisible() && tick.getTickType().equals(
                      TickType.MAJOR)) || (isMinorTickMarksVisible()
                      && tick.getTickType().equals(TickType.MINOR))) {
  
                  double ol = (tick.getTickType().equals(TickType.MINOR))
                          ? getMinorTickMarkOutsideLength()
                          : getTickMarkOutsideLength();
  
                  double il = (tick.getTickType().equals(TickType.MINOR))
                          ? getMinorTickMarkInsideLength()
                          : getTickMarkInsideLength();
  
                  float xx = (float) valueToJava2D(tick.getValue(), dataArea,
                          edge);
                  Line2D mark = null;
                  g2.setStroke(getTickMarkStroke());
                  g2.setPaint(getTickMarkPaint());
                  if (edge == RectangleEdge.LEFT) {
                      mark = new Line2D.Double(cursor - ol, xx, cursor + il, xx);
                  } else if (edge == RectangleEdge.RIGHT) {
                      mark = new Line2D.Double(cursor + ol, xx, cursor - il, xx);
                  } else if (edge == RectangleEdge.TOP) {
                      mark = new Line2D.Double(xx, cursor - ol, xx, cursor + il);
                  } else if (edge == RectangleEdge.BOTTOM) {
                      mark = new Line2D.Double(xx, cursor + ol, xx, cursor - il);
                  }
                  g2.draw(mark);
              }
          }
  
          // need to work out the space used by the tick labels...
          // so we can update the cursor...
          double used = 0.0;
          if (isTickLabelsVisible()) {
              if (edge == RectangleEdge.LEFT) {
                  used += findMaximumTickLabelWidth(ticks, g2, plotArea,
                          isVerticalTickLabels());
                  state.cursorLeft(used);
              }
              else if (edge == RectangleEdge.RIGHT) {
                  used = findMaximumTickLabelWidth(ticks, g2, plotArea,
                          isVerticalTickLabels());
                  state.cursorRight(used);
              }
              else if (edge == RectangleEdge.TOP) {
                  used = findMaximumTickLabelHeight(ticks, g2, plotArea,
                          isVerticalTickLabels());
                  state.cursorUp(used);
              }
              else if (edge == RectangleEdge.BOTTOM) {
                  used = findMaximumTickLabelHeight(ticks, g2, plotArea,
                          isVerticalTickLabels());
                  state.cursorDown(used);
              }
          }
  
          return state;
      }
  
      /**
       * Calculates the positions of the ticks for the axis, storing the results
       * in the tick list (ready for drawing).
       *
       * @param g2  the graphics device.
       * @param state  the axis state.
       * @param dataArea  the area inside the axes.
       * @param edge  the edge on which the axis is located.
       *
       * @return The list of ticks.
       */
      public abstract List<ValueTick> refreshTicks(Graphics2D g2, AxisState state,
                                                        Rectangle2D dataArea, RectangleEdge edge);
  
      /**
       * Returns the space required to draw the axis.
       *
       * @param g2  the graphics device.
       * @param plot  the plot that the axis belongs to.
       * @param plotArea  the area within which the plot should be drawn.
       * @param edge  the axis location.
       * @param space  the space already reserved (for other axes).
       *
       * @return The space required to draw the axis (including pre-reserved
       *         space).
       */
      @Override
      public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
              Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
  
          // create a new space object if one wasn't supplied...
          if (space == null) {
              space = new AxisSpace();
          }
  
          // if the axis is not visible, no additional space is required...
          if (!isVisible()) {
              return space;
          }
  
          // if the axis has a fixed dimension, return it...
          double dimension = getFixedDimension();
          if (dimension > 0.0) {
              space.add(dimension, edge);
              return space;
          }
  
          // calculate the max size of the tick labels (if visible)...
          double tickLabelHeight = 0.0;
          double tickLabelWidth = 0.0;
          if (isTickLabelsVisible()) {
              g2.setFont(getTickLabelFont());
              List<ValueTick> ticks = refreshTicks(g2, new AxisState(), plotArea, edge);
              if (RectangleEdge.isTopOrBottom(edge)) {
                  tickLabelHeight = findMaximumTickLabelHeight(ticks, g2,
                          plotArea, isVerticalTickLabels());
              }
              else if (RectangleEdge.isLeftOrRight(edge)) {
                  tickLabelWidth = findMaximumTickLabelWidth(ticks, g2, plotArea,
                          isVerticalTickLabels());
              }
          }
  
          // get the axis label size and update the space object...
          Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
          if (RectangleEdge.isTopOrBottom(edge)) {
              double labelHeight = labelEnclosure.getHeight();
              space.add(labelHeight + tickLabelHeight, edge);
          }
          else if (RectangleEdge.isLeftOrRight(edge)) {
              double labelWidth = labelEnclosure.getWidth();
              space.add(labelWidth + tickLabelWidth, edge);
          }
  
          return space;
  
      }
  
      /**
       * A utility method for determining the height of the tallest tick label.
       *
       * @param ticks  the ticks.
       * @param g2  the graphics device.
       * @param drawArea  the area within which the plot and axes should be drawn.
       * @param vertical  a flag that indicates whether or not the tick labels
       *                  are 'vertical'.
       *
       * @return The height of the tallest tick label.
       */
      protected double findMaximumTickLabelHeight(List<ValueTick> ticks, 
              Graphics2D g2, Rectangle2D drawArea, boolean vertical) {
  
          RectangleInsets insets = getTickLabelInsets();
          Font font = getTickLabelFont();
          double maxHeight = 0.0;
          if (vertical) {
              FontMetrics fm = g2.getFontMetrics(font);
              for (Tick tick : ticks) {
                  Rectangle2D labelBounds = TextUtilities.getTextBounds(
                          tick.getText(), g2, fm);
                  if (labelBounds.getWidth() + insets.getTop()
                          + insets.getBottom() > maxHeight) {
                      maxHeight = labelBounds.getWidth()
                              + insets.getTop() + insets.getBottom();
                  }
              }
          }
          else {
              LineMetrics metrics = font.getLineMetrics("ABCxyz",
                      g2.getFontRenderContext());
              maxHeight = metrics.getHeight()
                          + insets.getTop() + insets.getBottom();
          }
          return maxHeight;
  
      }
  
      /**
       * A utility method for determining the width of the widest tick label.
       *
       * @param ticks  the ticks.
       * @param g2  the graphics device.
       * @param drawArea  the area within which the plot and axes should be drawn.
       * @param vertical  a flag that indicates whether or not the tick labels
       *                  are 'vertical'.
       *
       * @return The width of the tallest tick label.
       */
      protected double findMaximumTickLabelWidth(List<ValueTick> ticks, 
              Graphics2D g2, Rectangle2D drawArea, boolean vertical) {
  
          RectangleInsets insets = getTickLabelInsets();
          Font font = getTickLabelFont();
          double maxWidth = 0.0;
          if (!vertical) {
              FontMetrics fm = g2.getFontMetrics(font);
              for (ValueTick tick : ticks) {
                  Rectangle2D labelBounds = TextUtilities.getTextBounds(
                          tick.getText(), g2, fm);
                  if (labelBounds.getWidth() + insets.getLeft()
                          + insets.getRight() > maxWidth) {
                      maxWidth = labelBounds.getWidth()
                              + insets.getLeft() + insets.getRight();
                  }
              }
          }
          else {
              LineMetrics metrics = font.getLineMetrics("ABCxyz",
                      g2.getFontRenderContext());
              maxWidth = metrics.getHeight()
                         + insets.getTop() + insets.getBottom();
          }
          return maxWidth;
  
      }
  
      /**
       * Returns a flag that controls the direction of values on the axis.
       * <P>
       * For a regular axis, values increase from left to right (for a horizontal
       * axis) and bottom to top (for a vertical axis).  When the axis is
       * 'inverted', the values increase in the opposite direction.
       *
       * @return The flag.
       *
       * @see #setInverted(boolean)
       */
      public boolean isInverted() {
          return this.inverted;
      }
  
      /**
       * Sets a flag that controls the direction of values on the axis, and
       * notifies registered listeners that the axis has changed.
       *
       * @param flag  the flag.
       *
       * @see #isInverted()
       */
      public void setInverted(boolean flag) {
          if (this.inverted != flag) {
              this.inverted = flag;
              fireChangeEvent();
          }
      }
  
      /**
       * Returns the flag that controls whether or not the axis range is
       * automatically adjusted to fit the data values.
       *
       * @return The flag.
       *
       * @see #setAutoRange(boolean)
       */
      public boolean isAutoRange() {
          return this.autoRange;
      }
  
      /**
       * Sets a flag that determines whether or not the axis range is
       * automatically adjusted to fit the data, and notifies registered
       * listeners that the axis has been modified.
       *
       * @param auto  the new value of the flag.
       *
       * @see #isAutoRange()
       */
      public void setAutoRange(boolean auto) {
          setAutoRange(auto, true);
      }
  
      /**
       * Sets the auto range attribute.  If the <code>notify</code> flag is set,
       * an {@link AxisChangeEvent} is sent to registered listeners.
       *
       * @param auto  the flag.
       * @param notify  notify listeners?
       *
       * @see #isAutoRange()
       */
      protected void setAutoRange(boolean auto, boolean notify) {
          if (this.autoRange != auto) {
              this.autoRange = auto;
              if (this.autoRange) {
                  autoAdjustRange();
              }
              if (notify) {
                  fireChangeEvent();
              }
          }
      }
  
      /**
       * Returns the minimum size allowed for the axis range when it is
       * automatically calculated.
       *
       * @return The minimum range.
       *
       * @see #setAutoRangeMinimumSize(double)
       */
      public double getAutoRangeMinimumSize() {
          return this.autoRangeMinimumSize;
      }
  
      /**
       * Sets the auto range minimum size and sends an {@link AxisChangeEvent}
       * to all registered listeners.
       *
       * @param size  the size.
       *
       * @see #getAutoRangeMinimumSize()
       */
      public void setAutoRangeMinimumSize(double size) {
          setAutoRangeMinimumSize(size, true);
      }
  
      /**
       * Sets the minimum size allowed for the axis range when it is
       * automatically calculated.
       * <p>
       * If requested, an {@link AxisChangeEvent} is forwarded to all registered
       * listeners.
       *
       * @param size  the new minimum.
       * @param notify  notify listeners?
       */
      public void setAutoRangeMinimumSize(double size, boolean notify) {
          if (size <= 0.0) {
              throw new IllegalArgumentException(
                  "NumberAxis.setAutoRangeMinimumSize(double): must be > 0.0.");
          }
          if (this.autoRangeMinimumSize != size) {
              this.autoRangeMinimumSize = size;
              if (this.autoRange) {
                  autoAdjustRange();
              }
              if (notify) {
                  fireChangeEvent();
              }
          }
  
      }
  
      /**
       * Returns the default auto range.
       *
       * @return The default auto range (never <code>null</code>).
       *
       * @see #setDefaultAutoRange(Range)
       *
       * @since 1.0.5
       */
      public Range getDefaultAutoRange() {
          return this.defaultAutoRange;
      }
  
      /**
       * Sets the default auto range and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       *
       * @param range  the range (<code>null</code> not permitted).
       *
       * @see #getDefaultAutoRange()
       *
       * @since 1.0.5
       */
      public void setDefaultAutoRange(Range range) {
          ParamChecks.nullNotPermitted(range, "range");
          this.defaultAutoRange = range;
          fireChangeEvent();
      }
  
      /**
       * Returns the lower margin for the axis, expressed as a percentage of the
       * axis range.  This controls the space added to the lower end of the axis
       * when the axis range is automatically calculated (it is ignored when the
       * axis range is set explicitly). The default value is 0.05 (five percent).
       *
       * @return The lower margin.
       *
       * @see #setLowerMargin(double)
       */
      public double getLowerMargin() {
          return this.lowerMargin;
      }
  
      /**
       * Sets the lower margin for the axis (as a percentage of the axis range)
       * and sends an {@link AxisChangeEvent} to all registered listeners.  This
       * margin is added only when the axis range is auto-calculated - if you set
       * the axis range manually, the margin is ignored.
       *
       * @param margin  the margin percentage (for example, 0.05 is five percent).
       *
       * @see #getLowerMargin()
       * @see #setUpperMargin(double)
       */
      public void setLowerMargin(double margin) {
          this.lowerMargin = margin;
          if (isAutoRange()) {
              autoAdjustRange();
          }
          fireChangeEvent();
      }
  
      /**
       * Returns the upper margin for the axis, expressed as a percentage of the
       * axis range.  This controls the space added to the lower end of the axis
       * when the axis range is automatically calculated (it is ignored when the
       * axis range is set explicitly). The default value is 0.05 (five percent).
       *
       * @return The upper margin.
       *
       * @see #setUpperMargin(double)
       */
      public double getUpperMargin() {
          return this.upperMargin;
      }
  
      /**
       * Sets the upper margin for the axis (as a percentage of the axis range)
       * and sends an {@link AxisChangeEvent} to all registered listeners.  This
       * margin is added only when the axis range is auto-calculated - if you set
       * the axis range manually, the margin is ignored.
       *
       * @param margin  the margin percentage (for example, 0.05 is five percent).
       *
       * @see #getLowerMargin()
       * @see #setLowerMargin(double)
       */
      public void setUpperMargin(double margin) {
          this.upperMargin = margin;
          if (isAutoRange()) {
              autoAdjustRange();
          }
          fireChangeEvent();
      }
  
      /**
       * Returns the fixed auto range.
       *
       * @return The length.
       *
       * @see #setFixedAutoRange(double)
       */
      public double getFixedAutoRange() {
          return this.fixedAutoRange;
      }
  
      /**
       * Sets the fixed auto range for the axis.
       *
       * @param length  the range length.
       *
       * @see #getFixedAutoRange()
       */
      public void setFixedAutoRange(double length) {
          this.fixedAutoRange = length;
          if (isAutoRange()) {
              autoAdjustRange();
          }
          fireChangeEvent();
      }
  
      /**
       * Returns the lower bound of the axis range.
       *
       * @return The lower bound.
       *
       * @see #setLowerBound(double)
       */
      public double getLowerBound() {
          return this.range.getLowerBound();
      }
  
      /**
       * Sets the lower bound for the axis range.  An {@link AxisChangeEvent} is
       * sent to all registered listeners.
       *
       * @param min  the new minimum.
       *
       * @see #getLowerBound()
       */
      public void setLowerBound(double min) {
          if (this.range.getUpperBound() > min) {
              setRange(new Range(min, this.range.getUpperBound()));
          }
          else {
              setRange(new Range(min, min + 1.0));
          }
      }
  
      /**
       * Returns the upper bound for the axis range.
       *
       * @return The upper bound.
       *
       * @see #setUpperBound(double)
       */
      public double getUpperBound() {
          return this.range.getUpperBound();
      }
  
      /**
       * Sets the upper bound for the axis range, and sends an
       * {@link AxisChangeEvent} to all registered listeners.
       *
       * @param max  the new maximum.
       *
       * @see #getUpperBound()
       */
      public void setUpperBound(double max) {
          if (this.range.getLowerBound() < max) {
              setRange(new Range(this.range.getLowerBound(), max));
          }
          else {
              setRange(max - 1.0, max);
          }
      }
  
      /**
       * Returns the range for the axis.
       *
       * @return The axis range (never <code>null</code>).
       *
       * @see #setRange(Range)
       */
      public Range getRange() {
          return this.range;
      }
  
      /**
       * Sets the range attribute and sends an {@link AxisChangeEvent} to all
       * registered listeners.  As a side-effect, the auto-range flag is set to
       * <code>false</code>.
       *
       * @param range  the range (<code>null</code> not permitted).
       *
       * @see #getRange()
       */
      public void setRange(Range range) {
          // defer argument checking
          setRange(range, true, true);
      }
  
      /**
       * Sets the range for the axis, if requested, sends an
       * {@link AxisChangeEvent} to all registered listeners.  As a side-effect,
       * the auto-range flag is set to <code>false</code> (optional).
       *
       * @param range  the range (<code>null</code> not permitted).
       * @param turnOffAutoRange  a flag that controls whether or not the auto
       *                          range is turned off.
       * @param notify  a flag that controls whether or not listeners are
       *                notified.
       *
       * @see #getRange()
       */
      public void setRange(Range range, boolean turnOffAutoRange,
                           boolean notify) {
          ParamChecks.nullNotPermitted(range, "range");
          if (turnOffAutoRange) {
              this.autoRange = false;
          }
          this.range = range;
          if (notify) {
              fireChangeEvent();
          }
      }
  
      /**
       * Sets the axis range and sends an {@link AxisChangeEvent} to all
       * registered listeners.  As a side-effect, the auto-range flag is set to
       * <code>false</code>.
       *
       * @param lower  the lower axis limit.
       * @param upper  the upper axis limit.
       *
       * @see #getRange()
       * @see #setRange(Range)
       */
      public void setRange(double lower, double upper) {
          setRange(new Range(lower, upper));
      }
  
      /**
       * Sets the range for the axis (after first adding the current margins to
       * the specified range) and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       *
       * @param range  the range (<code>null</code> not permitted).
       */
      public void setRangeWithMargins(Range range) {
          setRangeWithMargins(range, true, true);
      }
  
      /**
       * Sets the range for the axis after first adding the current margins to
       * the range and, if requested, sends an {@link AxisChangeEvent} to all
       * registered listeners.  As a side-effect, the auto-range flag is set to
       * <code>false</code> (optional).
       *
       * @param range  the range (excluding margins, <code>null</code> not
       *               permitted).
       * @param turnOffAutoRange  a flag that controls whether or not the auto
       *                          range is turned off.
       * @param notify  a flag that controls whether or not listeners are
       *                notified.
       */
      public void setRangeWithMargins(Range range, boolean turnOffAutoRange,
                                      boolean notify) {
          ParamChecks.nullNotPermitted(range, "range");
          setRange(Range.expand(range, getLowerMargin(), getUpperMargin()),
                  turnOffAutoRange, notify);
      }
  
      /**
       * Sets the axis range (after first adding the current margins to the
       * range) and sends an {@link AxisChangeEvent} to all registered listeners.
       * As a side-effect, the auto-range flag is set to <code>false</code>.
       *
       * @param lower  the lower axis limit.
       * @param upper  the upper axis limit.
       */
      public void setRangeWithMargins(double lower, double upper) {
          setRangeWithMargins(new Range(lower, upper));
      }
  
      /**
       * Sets the axis range, where the new range is 'size' in length, and
       * centered on 'value'.
       *
       * @param value  the central value.
       * @param length  the range length.
       */
      public void setRangeAboutValue(double value, double length) {
          setRange(new Range(value - length / 2, value + length / 2));
      }
  
      /**
       * Returns a flag indicating whether or not the tick unit is automatically
       * selected from a range of standard tick units.
       *
       * @return A flag indicating whether or not the tick unit is automatically
       *         selected.
       *
       * @see #setAutoTickUnitSelection(boolean)
       */
      public boolean isAutoTickUnitSelection() {
          return this.autoTickUnitSelection;
      }
  
      /**
       * Sets a flag indicating whether or not the tick unit is automatically
       * selected from a range of standard tick units.  If the flag is changed,
       * registered listeners are notified that the chart has changed.
       *
       * @param flag  the new value of the flag.
       *
       * @see #isAutoTickUnitSelection()
       */
      public void setAutoTickUnitSelection(boolean flag) {
          setAutoTickUnitSelection(flag, true);
      }
  
      /**
       * Sets a flag indicating whether or not the tick unit is automatically
       * selected from a range of standard tick units.
       *
       * @param flag  the new value of the flag.
       * @param notify  notify listeners?
       *
       * @see #isAutoTickUnitSelection()
       */
      public void setAutoTickUnitSelection(boolean flag, boolean notify) {
  
          if (this.autoTickUnitSelection != flag) {
              this.autoTickUnitSelection = flag;
              if (notify) {
                  fireChangeEvent();
              }
          }
      }
  
      /**
       * Returns the source for obtaining standard tick units for the axis.
       *
       * @return The source (possibly <code>null</code>).
       *
       * @see #setStandardTickUnits(TickUnitSource)
       */
      public TickUnitSource getStandardTickUnits() {
          return this.standardTickUnits;
      }
  
      /**
       * Sets the source for obtaining standard tick units for the axis and sends
       * an {@link AxisChangeEvent} to all registered listeners.  The axis will
       * try to select the smallest tick unit from the source that does not cause
       * the tick labels to overlap (see also the
       * {@link #setAutoTickUnitSelection(boolean)} method.
       *
       * @param source  the source for standard tick units (<code>null</code>
       *                permitted).
       *
       * @see #getStandardTickUnits()
       */
      public void setStandardTickUnits(TickUnitSource source) {
          this.standardTickUnits = source;
          fireChangeEvent();
      }
  
      /**
       * Returns the number of minor tick marks to display.
       *
       * @return The number of minor tick marks to display.
       *
       * @see #setMinorTickCount(int)
       *
       * @since 1.0.12
       */
      public int getMinorTickCount() {
          return this.minorTickCount;
      }
  
      /**
       * Sets the number of minor tick marks to display, and sends an
       * {@link AxisChangeEvent} to all registered listeners.
       *
       * @param count  the count.
       *
       * @see #getMinorTickCount()
       *
       * @since 1.0.12
       */
      public void setMinorTickCount(int count) {
          this.minorTickCount = count;
          fireChangeEvent();
      }
  
      /**
       * Converts a data value to a coordinate in Java2D space, assuming that the
       * axis runs along one edge of the specified dataArea.
       * <p>
       * Note that it is possible for the coordinate to fall outside the area.
       *
       * @param value  the data value.
       * @param area  the area for plotting the data.
       * @param edge  the edge along which the axis lies.
       *
       * @return The Java2D coordinate.
       *
       * @see #java2DToValue(double, Rectangle2D, RectangleEdge)
       */
      public abstract double valueToJava2D(double value, Rectangle2D area,
                                           RectangleEdge edge);
  
      /**
       * Converts a length in data coordinates into the corresponding length in
       * Java2D coordinates.
       *
       * @param length  the length.
       * @param area  the plot area.
       * @param edge  the edge along which the axis lies.
       *
       * @return The length in Java2D coordinates.
       */
      public double lengthToJava2D(double length, Rectangle2D area,
                                   RectangleEdge edge) {
          double zero = valueToJava2D(0.0, area, edge);
          double l = valueToJava2D(length, area, edge);
          return Math.abs(l - zero);
      }
  
      /**
       * Converts a coordinate in Java2D space to the corresponding data value,
       * assuming that the axis runs along one edge of the specified dataArea.
       *
       * @param java2DValue  the coordinate in Java2D space.
       * @param area  the area in which the data is plotted.
       * @param edge  the edge along which the axis lies.
       *
       * @return The data value.
       *
       * @see #valueToJava2D(double, Rectangle2D, RectangleEdge)
       */
      public abstract double java2DToValue(double java2DValue, Rectangle2D area, 
                                           RectangleEdge edge);
  
      /**
       * Automatically sets the axis range to fit the range of values in the
       * dataset.  Sometimes this can depend on the renderer used as well (for
       * example, the renderer may "stack" values, requiring an axis range
       * greater than otherwise necessary).
       */
      protected abstract void autoAdjustRange();
  
      /**
       * Centers the axis range about the specified value and sends an
       * {@link AxisChangeEvent} to all registered listeners.
       *
       * @param value  the center value.
       */
      public void centerRange(double value) {
          double central = this.range.getCentralValue();
          Range adjusted = new Range(this.range.getLowerBound() + value - central,
                  this.range.getUpperBound() + value - central);
          setRange(adjusted);
      }
  
      /**
       * Increases or decreases the axis range by the specified percentage about
       * the central value and sends an {@link AxisChangeEvent} to all registered
       * listeners.
       * <P>
       * To double the length of the axis range, use 200% (2.0).
       * To halve the length of the axis range, use 50% (0.5).
       *
       * @param percent  the resize factor.
       *
       * @see #resizeRange(double, double)
       */
      public void resizeRange(double percent) {
          resizeRange(percent, this.range.getCentralValue());
      }
  
      /**
       * Increases or decreases the axis range by the specified percentage about
       * the specified anchor value and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       * <P>
       * To double the length of the axis range, use 200% (2.0).
       * To halve the length of the axis range, use 50% (0.5).
       *
       * @param percent  the resize factor.
       * @param anchorValue  the new central value after the resize.
       *
       * @see #resizeRange(double)
       */
      public void resizeRange(double percent, double anchorValue) {
          if (percent > 0.0) {
              double halfLength = this.range.getLength() * percent / 2;
              Range adjusted = new Range(anchorValue - halfLength,
                      anchorValue + halfLength);
              setRange(adjusted);
          }
          else {
              setAutoRange(true);
          }
      }
  
      /**
       * Increases or decreases the axis range by the specified percentage about
       * the specified anchor value and sends an {@link AxisChangeEvent} to all
       * registered listeners.
       * <P>
       * To double the length of the axis range, use 200% (2.0).
       * To halve the length of the axis range, use 50% (0.5).
       *
       * @param percent  the resize factor.
       * @param anchorValue  the new central value after the resize.
       *
       * @see #resizeRange(double)
       *
       * @since 1.0.13
       */
      public void resizeRange2(double percent, double anchorValue) {
          if (percent > 0.0) {
              double left = anchorValue - getLowerBound();
              double right = getUpperBound() - anchorValue;
              Range adjusted = new Range(anchorValue - left * percent,
                      anchorValue + right * percent);
              setRange(adjusted);
          }
          else {
              setAutoRange(true);
          }
      }
  
      /**
       * Zooms in on the current range.
       *
       * @param lowerPercent  the new lower bound.
       * @param upperPercent  the new upper bound.
       */
      public void zoomRange(double lowerPercent, double upperPercent) {
          double start = this.range.getLowerBound();
          double length = this.range.getLength();
          Range adjusted;
          if (isInverted()) {
              adjusted = new Range(start + (length * (1 - upperPercent)),
                                   start + (length * (1 - lowerPercent)));
          }
          else {
              adjusted = new Range(start + length * lowerPercent,
                      start + length * upperPercent);
          }
          setRange(adjusted);
      }
  
      /**
       * Slides the axis range by the specified percentage.
       *
       * @param percent  the percentage.
       *
       * @since 1.0.13
       */
      public void pan(double percent) {
          Range r = getRange();
          double length = range.getLength();
          double adj = length * percent;
          double lower = r.getLowerBound() + adj;
          double upper = r.getUpperBound() + adj;
          setRange(lower, upper);
      }
  
      /**
       * Returns the auto tick index.
       *
       * @return The auto tick index.
       *
       * @see #setAutoTickIndex(int)
       */
      protected int getAutoTickIndex() {
          return this.autoTickIndex;
      }
  
      /**
       * Sets the auto tick index.
       *
       * @param index  the new value.
       *
       * @see #getAutoTickIndex()
       */
      protected void setAutoTickIndex(int index) {
          this.autoTickIndex = index;
      }
  
      /**
       * Tests the axis for equality with an arbitrary object.
       *
       * @param obj  the object (<code>null</code> permitted).
       *
       * @return <code>true</code> or <code>false</code>.
       */
      @Override
      public boolean equals(Object obj) {
          if (obj == this) {
              return true;
          }
          if (!(obj instanceof ValueAxis)) {
              return false;
          }
          ValueAxis that = (ValueAxis) obj;
          if (this.positiveArrowVisible != that.positiveArrowVisible) {
              return false;
          }
          if (this.negativeArrowVisible != that.negativeArrowVisible) {
              return false;
          }
          if (this.inverted != that.inverted) {
              return false;
          }
          // if autoRange is true, then the current range is irrelevant
          if (!this.autoRange && !ObjectUtilities.equal(this.range, that.range)) {
              return false;
          }
          if (this.autoRange != that.autoRange) {
              return false;
          }
          if (this.autoRangeMinimumSize != that.autoRangeMinimumSize) {
              return false;
          }
          if (!this.defaultAutoRange.equals(that.defaultAutoRange)) {
              return false;
          }
          if (this.upperMargin != that.upperMargin) {
              return false;
          }
          if (this.lowerMargin != that.lowerMargin) {
              return false;
          }
          if (this.fixedAutoRange != that.fixedAutoRange) {
              return false;
          }
          if (this.autoTickUnitSelection != that.autoTickUnitSelection) {
              return false;
          }
          if (!ObjectUtilities.equal(this.standardTickUnits,
                  that.standardTickUnits)) {
              return false;
          }
          if (this.verticalTickLabels != that.verticalTickLabels) {
              return false;
          }
          if (this.minorTickCount != that.minorTickCount) {
              return false;
          }
          return super.equals(obj);
      }
  
      /**
       * Returns a clone of the object.
       *
       * @return A clone.
       *
       * @throws CloneNotSupportedException if some component of the axis does
       *         not support cloning.
       */
      @Override
      public Object clone() throws CloneNotSupportedException {
          ValueAxis clone = (ValueAxis) super.clone();
          return clone;
      }
  
      /**
       * Provides serialization support.
       *
       * @param stream  the output stream.
       *
       * @throws IOException  if there is an I/O error.
       */
      private void writeObject(ObjectOutputStream stream) throws IOException {
          stream.defaultWriteObject();
          SerialUtilities.writeShape(this.upArrow, stream);
          SerialUtilities.writeShape(this.downArrow, stream);
          SerialUtilities.writeShape(this.leftArrow, stream);
          SerialUtilities.writeShape(this.rightArrow, stream);
      }
  
      /**
       * Provides serialization support.
       *
       * @param stream  the input stream.
       *
       * @throws IOException  if there is an I/O error.
       * @throws ClassNotFoundException  if there is a classpath problem.
       */
      private void readObject(ObjectInputStream stream)
              throws IOException, ClassNotFoundException {
  
          stream.defaultReadObject();
          this.upArrow = SerialUtilities.readShape(stream);
          this.downArrow = SerialUtilities.readShape(stream);
          this.leftArrow = SerialUtilities.readShape(stream);
          this.rightArrow = SerialUtilities.readShape(stream);
      }
  
  }
  