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