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