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