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