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 * Axis.java
029 * ---------
030 * (C) Copyright 2000-present, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Bill Kelemen;
034 *                   Nicolas Brodu;
035 *                   Peter Kolb (patches 1934255 and 2603321);
036 *                   Andrew Mickish (patch 1870189); 
037 *
038 */
039
040package org.jfree.chart.axis;
041
042import java.awt.BasicStroke;
043import java.awt.Color;
044import java.awt.Font;
045import java.awt.FontMetrics;
046import java.awt.Graphics2D;
047import java.awt.Paint;
048import java.awt.RenderingHints;
049import java.awt.Shape;
050import java.awt.Stroke;
051import java.awt.font.TextLayout;
052import java.awt.geom.AffineTransform;
053import java.awt.geom.Line2D;
054import java.awt.geom.Rectangle2D;
055import java.io.IOException;
056import java.io.ObjectInputStream;
057import java.io.ObjectOutputStream;
058import java.io.Serializable;
059import java.text.AttributedString;
060import java.util.Arrays;
061import java.util.EventListener;
062import java.util.List;
063import java.util.Objects;
064
065import javax.swing.event.EventListenerList;
066
067import org.jfree.chart.entity.AxisEntity;
068import org.jfree.chart.entity.EntityCollection;
069import org.jfree.chart.event.AxisChangeEvent;
070import org.jfree.chart.event.AxisChangeListener;
071import org.jfree.chart.plot.Plot;
072import org.jfree.chart.plot.PlotRenderingInfo;
073import org.jfree.chart.text.AttributedStringUtils;
074import org.jfree.chart.text.TextUtils;
075import org.jfree.chart.ui.RectangleEdge;
076import org.jfree.chart.ui.RectangleInsets;
077import org.jfree.chart.ui.TextAnchor;
078import org.jfree.chart.util.AttrStringUtils;
079import org.jfree.chart.util.PaintUtils;
080import org.jfree.chart.util.Args;
081import org.jfree.chart.util.SerialUtils;
082
083/**
084 * The base class for all axes in JFreeChart.  Subclasses are divided into
085 * those that display values ({@link ValueAxis}) and those that display
086 * categories ({@link CategoryAxis}).
087 */
088public abstract class Axis implements Cloneable, Serializable {
089
090    /** For serialization. */
091    private static final long serialVersionUID = 7719289504573298271L;
092
093    /** The default axis visibility ({@code true}). */
094    public static final boolean DEFAULT_AXIS_VISIBLE = true;
095
096    /** The default axis label font ({@code Font("SansSerif", Font.PLAIN, 12)}). */
097    public static final Font DEFAULT_AXIS_LABEL_FONT = new Font(
098            "SansSerif", Font.PLAIN, 12);
099
100    /** The default axis label paint ({@code Color.BLACK}). */
101    public static final Paint DEFAULT_AXIS_LABEL_PAINT = Color.BLACK;
102
103    /** The default axis label insets ({@code RectangleInsets(3.0, 3.0, 3.0, 3.0)}). */
104    public static final RectangleInsets DEFAULT_AXIS_LABEL_INSETS
105            = new RectangleInsets(3.0, 3.0, 3.0, 3.0);
106
107    /** The default axis line paint ({@code Color.GRAY}). */
108    public static final Paint DEFAULT_AXIS_LINE_PAINT = Color.GRAY;
109
110    /** The default axis line stroke ({@code BasicStroke(0.5f)}). */
111    public static final Stroke DEFAULT_AXIS_LINE_STROKE = new BasicStroke(0.5f);
112
113    /** The default tick labels visibility ({@code true}). */
114    public static final boolean DEFAULT_TICK_LABELS_VISIBLE = true;
115
116    /** The default tick label font ({@code Font("SansSerif", Font.PLAIN, 10)}). */
117    public static final Font DEFAULT_TICK_LABEL_FONT = new Font("SansSerif",
118            Font.PLAIN, 10);
119
120    /** The default tick label paint ({@code Color.BLACK}). */
121    public static final Paint DEFAULT_TICK_LABEL_PAINT = Color.BLACK;
122
123    /** The default tick label insets ({@code RectangleInsets(2.0, 4.0, 2.0, 4.0)}). */
124    public static final RectangleInsets DEFAULT_TICK_LABEL_INSETS
125            = new RectangleInsets(2.0, 4.0, 2.0, 4.0);
126
127    /** The default tick marks visible ({@code true}). */
128    public static final boolean DEFAULT_TICK_MARKS_VISIBLE = true;
129
130    /** The default tick stroke ({@code BasicStroke(0.5f)}). */
131    public static final Stroke DEFAULT_TICK_MARK_STROKE = new BasicStroke(0.5f);
132
133    /** The default tick paint ({@code Color.GRAY}). */
134    public static final Paint DEFAULT_TICK_MARK_PAINT = Color.GRAY;
135
136    /** The default tick mark inside length ({@code 0.0f}). */
137    public static final float DEFAULT_TICK_MARK_INSIDE_LENGTH = 0.0f;
138
139    /** The default tick mark outside length ({@code 2.0f}). */
140    public static final float DEFAULT_TICK_MARK_OUTSIDE_LENGTH = 2.0f;
141
142    /** A flag indicating whether or not the axis is visible. */
143    private boolean visible;
144
145    /** The label for the axis. */
146    private String label;
147    
148    /** 
149     * An attributed label for the axis (overrides label if non-null).
150     * We have to use this override method to preserve the API compatibility.
151     */
152    private transient AttributedString attributedLabel;
153
154    /** The font for displaying the axis label. */
155    private Font labelFont;
156
157    /** The paint for drawing the axis label. */
158    private transient Paint labelPaint;
159
160    /** The insets for the axis label. */
161    private RectangleInsets labelInsets;
162
163    /** The label angle. */
164    private double labelAngle;
165    
166    /** The axis label location (new in 1.0.16). */
167    private AxisLabelLocation labelLocation;
168
169    /** A flag that controls whether or not the axis line is visible. */
170    private boolean axisLineVisible;
171
172    /** The stroke used for the axis line. */
173    private transient Stroke axisLineStroke;
174
175    /** The paint used for the axis line. */
176    private transient Paint axisLinePaint;
177
178    /**
179     * A flag that indicates whether or not tick labels are visible for the
180     * axis.
181     */
182    private boolean tickLabelsVisible;
183
184    /** The font used to display the tick labels. */
185    private Font tickLabelFont;
186
187    /** The color used to display the tick labels. */
188    private transient Paint tickLabelPaint;
189
190    /** The blank space around each tick label. */
191    private RectangleInsets tickLabelInsets;
192
193    /**
194     * A flag that indicates whether or not major tick marks are visible for
195     * the axis.
196     */
197    private boolean tickMarksVisible;
198
199    /**
200     * The length of the major tick mark inside the data area (zero
201     * permitted).
202     */
203    private float tickMarkInsideLength;
204
205    /**
206     * The length of the major tick mark outside the data area (zero
207     * permitted).
208     */
209    private float tickMarkOutsideLength;
210
211    /**
212     * A flag that indicates whether or not minor tick marks are visible for the
213     * axis.
214     */
215    private boolean minorTickMarksVisible;
216
217    /**
218     * The length of the minor tick mark inside the data area (zero permitted).
219     */
220    private float minorTickMarkInsideLength;
221
222    /**
223     * The length of the minor tick mark outside the data area (zero permitted).
224     */
225    private float minorTickMarkOutsideLength;
226
227    /** The stroke used to draw tick marks. */
228    private transient Stroke tickMarkStroke;
229
230    /** The paint used to draw tick marks. */
231    private transient Paint tickMarkPaint;
232
233    /** The fixed (horizontal or vertical) dimension for the axis. */
234    private double fixedDimension;
235
236    /**
237     * A reference back to the plot that the axis is assigned to (can be
238     * {@code null}).
239     */
240    private transient Plot plot;
241
242    /** Storage for registered listeners. */
243    private transient EventListenerList listenerList;
244
245    /**
246     * Constructs an axis with the specific label and default values for other
247     * attributes.  
248     *
249     * @param label  the axis label ({@code null} permitted).
250     */
251    protected Axis(String label) {
252
253        this.label = label;
254        this.visible = DEFAULT_AXIS_VISIBLE;
255        this.labelFont = DEFAULT_AXIS_LABEL_FONT;
256        this.labelPaint = DEFAULT_AXIS_LABEL_PAINT;
257        this.labelInsets = DEFAULT_AXIS_LABEL_INSETS;
258        this.labelAngle = 0.0;
259        this.labelLocation = AxisLabelLocation.MIDDLE;
260
261        this.axisLineVisible = true;
262        this.axisLinePaint = DEFAULT_AXIS_LINE_PAINT;
263        this.axisLineStroke = DEFAULT_AXIS_LINE_STROKE;
264
265        this.tickLabelsVisible = DEFAULT_TICK_LABELS_VISIBLE;
266        this.tickLabelFont = DEFAULT_TICK_LABEL_FONT;
267        this.tickLabelPaint = DEFAULT_TICK_LABEL_PAINT;
268        this.tickLabelInsets = DEFAULT_TICK_LABEL_INSETS;
269
270        this.tickMarksVisible = DEFAULT_TICK_MARKS_VISIBLE;
271        this.tickMarkStroke = DEFAULT_TICK_MARK_STROKE;
272        this.tickMarkPaint = DEFAULT_TICK_MARK_PAINT;
273        this.tickMarkInsideLength = DEFAULT_TICK_MARK_INSIDE_LENGTH;
274        this.tickMarkOutsideLength = DEFAULT_TICK_MARK_OUTSIDE_LENGTH;
275
276        this.minorTickMarksVisible = false;
277        this.minorTickMarkInsideLength = 0.0f;
278        this.minorTickMarkOutsideLength = 2.0f;
279
280        this.plot = null;
281
282        this.listenerList = new EventListenerList();
283    }
284
285    /**
286     * Returns {@code true} if the axis is visible, and
287     * {@code false} otherwise.
288     *
289     * @return A boolean.
290     *
291     * @see #setVisible(boolean)
292     */
293    public boolean isVisible() {
294        return this.visible;
295    }
296
297    /**
298     * Sets a flag that controls whether or not the axis is visible and sends
299     * an {@link AxisChangeEvent} to all registered listeners.
300     *
301     * @param flag  the flag.
302     *
303     * @see #isVisible()
304     */
305    public void setVisible(boolean flag) {
306        if (flag != this.visible) {
307            this.visible = flag;
308            fireChangeEvent();
309        }
310    }
311
312    /**
313     * Returns the label for the axis.
314     *
315     * @return The label for the axis ({@code null} possible).
316     *
317     * @see #getLabelFont()
318     * @see #getLabelPaint()
319     * @see #setLabel(String)
320     */
321    public String getLabel() {
322        return this.label;
323    }
324
325    /**
326     * Sets the label for the axis and sends an {@link AxisChangeEvent} to all
327     * registered listeners.
328     *
329     * @param label  the new label ({@code null} permitted).
330     *
331     * @see #getLabel()
332     * @see #setLabelFont(Font)
333     * @see #setLabelPaint(Paint)
334     */
335    public void setLabel(String label) {
336        this.label = label;
337        fireChangeEvent();
338    }
339
340    /**
341     * Returns the attributed label (the returned value is a copy, so 
342     * modifying it will not impact the state of the axis).  The default value 
343     * is {@code null}.
344     * 
345     * @return The attributed label (possibly {@code null}).
346     */
347    public AttributedString getAttributedLabel() {
348        if (this.attributedLabel != null) {
349            return new AttributedString(this.attributedLabel.getIterator());
350        } else {
351            return null;
352        }
353    }
354    
355    /**
356     * Sets the attributed label for the axis and sends an 
357     * {@link AxisChangeEvent} to all registered listeners.  This is a 
358     * convenience method that converts the string into an 
359     * {@code AttributedString} using the current font attributes.
360     * 
361     * @param label  the label ({@code null} permitted).
362     */
363    public void setAttributedLabel(String label) {
364        setAttributedLabel(createAttributedLabel(label));    
365    }
366    
367    /**
368     * Sets the attributed label for the axis and sends an 
369     * {@link AxisChangeEvent} to all registered listeners.
370     * 
371     * @param label  the label ({@code null} permitted).
372     */
373    public void setAttributedLabel(AttributedString label) {
374        if (label != null) {
375            this.attributedLabel = new AttributedString(label.getIterator());
376        } else {
377            this.attributedLabel = null;
378        }
379        fireChangeEvent();
380    }
381    
382    /**
383     * Creates and returns an {@code AttributedString} with the specified
384     * text and the labelFont and labelPaint applied as attributes.
385     * 
386     * @param label  the label ({@code null} permitted).
387     * 
388     * @return An attributed string or {@code null}.
389     */
390    public AttributedString createAttributedLabel(String label) {
391        if (label == null) {
392            return null;
393        }
394        AttributedString s = new AttributedString(label);
395        s.addAttributes(this.labelFont.getAttributes(), 0, label.length());
396        return s;
397    }
398    
399    /**
400     * Returns the font for the axis label.
401     *
402     * @return The font (never {@code null}).
403     *
404     * @see #setLabelFont(Font)
405     */
406    public Font getLabelFont() {
407        return this.labelFont;
408    }
409
410    /**
411     * Sets the font for the axis label and sends an {@link AxisChangeEvent}
412     * to all registered listeners.
413     *
414     * @param font  the font ({@code null} not permitted).
415     *
416     * @see #getLabelFont()
417     */
418    public void setLabelFont(Font font) {
419        Args.nullNotPermitted(font, "font");
420        if (!this.labelFont.equals(font)) {
421            this.labelFont = font;
422            fireChangeEvent();
423        }
424    }
425
426    /**
427     * Returns the color/shade used to draw the axis label.
428     *
429     * @return The paint (never {@code null}).
430     *
431     * @see #setLabelPaint(Paint)
432     */
433    public Paint getLabelPaint() {
434        return this.labelPaint;
435    }
436
437    /**
438     * Sets the paint used to draw the axis label and sends an
439     * {@link AxisChangeEvent} to all registered listeners.
440     *
441     * @param paint  the paint ({@code null} not permitted).
442     *
443     * @see #getLabelPaint()
444     */
445    public void setLabelPaint(Paint paint) {
446        Args.nullNotPermitted(paint, "paint");
447        this.labelPaint = paint;
448        fireChangeEvent();
449    }
450
451    /**
452     * Returns the insets for the label (that is, the amount of blank space
453     * that should be left around the label).
454     *
455     * @return The label insets (never {@code null}).
456     *
457     * @see #setLabelInsets(RectangleInsets)
458     */
459    public RectangleInsets getLabelInsets() {
460        return this.labelInsets;
461    }
462
463    /**
464     * Sets the insets for the axis label, and sends an {@link AxisChangeEvent}
465     * to all registered listeners.
466     *
467     * @param insets  the insets ({@code null} not permitted).
468     *
469     * @see #getLabelInsets()
470     */
471    public void setLabelInsets(RectangleInsets insets) {
472        setLabelInsets(insets, true);
473    }
474
475    /**
476     * Sets the insets for the axis label, and sends an {@link AxisChangeEvent}
477     * to all registered listeners.
478     *
479     * @param insets  the insets ({@code null} not permitted).
480     * @param notify  notify listeners?
481     */
482    public void setLabelInsets(RectangleInsets insets, boolean notify) {
483        Args.nullNotPermitted(insets, "insets");
484        if (!insets.equals(this.labelInsets)) {
485            this.labelInsets = insets;
486            if (notify) {
487                fireChangeEvent();
488            }
489        }
490    }
491
492    /**
493     * Returns the angle of the axis label.
494     *
495     * @return The angle (in radians).
496     *
497     * @see #setLabelAngle(double)
498     */
499    public double getLabelAngle() {
500        return this.labelAngle;
501    }
502
503    /**
504     * Sets the angle for the label and sends an {@link AxisChangeEvent} to all
505     * registered listeners.
506     *
507     * @param angle  the angle (in radians).
508     *
509     * @see #getLabelAngle()
510     */
511    public void setLabelAngle(double angle) {
512        this.labelAngle = angle;
513        fireChangeEvent();
514    }
515    
516    /**
517     * Returns the location of the axis label.  The default is
518     * {@link AxisLabelLocation#MIDDLE}.
519     * 
520     * @return The location of the axis label (never {@code null}).
521     */
522    public AxisLabelLocation getLabelLocation() {
523        return this.labelLocation;
524    }
525    
526    /**
527     * Sets the axis label location and sends an {@link AxisChangeEvent} to
528     * all registered listeners.
529     * 
530     * @param location  the new location ({@code null} not permitted).
531     */
532    public void setLabelLocation(AxisLabelLocation location) {
533        Args.nullNotPermitted(location, "location");
534        this.labelLocation = location;
535        fireChangeEvent();
536    }
537
538    /**
539     * A flag that controls whether or not the axis line is drawn.
540     *
541     * @return A boolean.
542     *
543     * @see #getAxisLinePaint()
544     * @see #getAxisLineStroke()
545     * @see #setAxisLineVisible(boolean)
546     */
547    public boolean isAxisLineVisible() {
548        return this.axisLineVisible;
549    }
550
551    /**
552     * Sets a flag that controls whether or not the axis line is visible and
553     * sends an {@link AxisChangeEvent} to all registered listeners.
554     *
555     * @param visible  the flag.
556     *
557     * @see #isAxisLineVisible()
558     * @see #setAxisLinePaint(Paint)
559     * @see #setAxisLineStroke(Stroke)
560     */
561    public void setAxisLineVisible(boolean visible) {
562        this.axisLineVisible = visible;
563        fireChangeEvent();
564    }
565
566    /**
567     * Returns the paint used to draw the axis line.
568     *
569     * @return The paint (never {@code null}).
570     *
571     * @see #setAxisLinePaint(Paint)
572     */
573    public Paint getAxisLinePaint() {
574        return this.axisLinePaint;
575    }
576
577    /**
578     * Sets the paint used to draw the axis line and sends an
579     * {@link AxisChangeEvent} to all registered listeners.
580     *
581     * @param paint  the paint ({@code null} not permitted).
582     *
583     * @see #getAxisLinePaint()
584     */
585    public void setAxisLinePaint(Paint paint) {
586        Args.nullNotPermitted(paint, "paint");
587        this.axisLinePaint = paint;
588        fireChangeEvent();
589    }
590
591    /**
592     * Returns the stroke used to draw the axis line.
593     *
594     * @return The stroke (never {@code null}).
595     *
596     * @see #setAxisLineStroke(Stroke)
597     */
598    public Stroke getAxisLineStroke() {
599        return this.axisLineStroke;
600    }
601
602    /**
603     * Sets the stroke used to draw the axis line and sends an
604     * {@link AxisChangeEvent} to all registered listeners.
605     *
606     * @param stroke  the stroke ({@code null} not permitted).
607     *
608     * @see #getAxisLineStroke()
609     */
610    public void setAxisLineStroke(Stroke stroke) {
611        Args.nullNotPermitted(stroke, "stroke");
612        this.axisLineStroke = stroke;
613        fireChangeEvent();
614    }
615
616    /**
617     * Returns a flag indicating whether or not the tick labels are visible.
618     *
619     * @return The flag.
620     *
621     * @see #getTickLabelFont()
622     * @see #getTickLabelPaint()
623     * @see #setTickLabelsVisible(boolean)
624     */
625    public boolean isTickLabelsVisible() {
626        return this.tickLabelsVisible;
627    }
628
629    /**
630     * Sets the flag that determines whether or not the tick labels are
631     * visible and sends an {@link AxisChangeEvent} to all registered
632     * listeners.
633     *
634     * @param flag  the flag.
635     *
636     * @see #isTickLabelsVisible()
637     * @see #setTickLabelFont(Font)
638     * @see #setTickLabelPaint(Paint)
639     */
640    public void setTickLabelsVisible(boolean flag) {
641
642        if (flag != this.tickLabelsVisible) {
643            this.tickLabelsVisible = flag;
644            fireChangeEvent();
645        }
646
647    }
648
649    /**
650     * Returns the flag that indicates whether or not the minor tick marks are
651     * showing.
652     *
653     * @return The flag that indicates whether or not the minor tick marks are
654     *         showing.
655     *
656     * @see #setMinorTickMarksVisible(boolean)
657     */
658    public boolean isMinorTickMarksVisible() {
659        return this.minorTickMarksVisible;
660    }
661
662    /**
663     * Sets the flag that indicates whether or not the minor tick marks are 
664     * showing and sends an {@link AxisChangeEvent} to all registered
665     * listeners.
666     *
667     * @param flag  the flag.
668     *
669     * @see #isMinorTickMarksVisible()
670     */
671    public void setMinorTickMarksVisible(boolean flag) {
672        if (flag != this.minorTickMarksVisible) {
673            this.minorTickMarksVisible = flag;
674            fireChangeEvent();
675        }
676    }
677
678    /**
679     * Returns the font used for the tick labels (if showing).
680     *
681     * @return The font (never {@code null}).
682     *
683     * @see #setTickLabelFont(Font)
684     */
685    public Font getTickLabelFont() {
686        return this.tickLabelFont;
687    }
688
689    /**
690     * Sets the font for the tick labels and sends an {@link AxisChangeEvent}
691     * to all registered listeners.
692     *
693     * @param font  the font ({@code null} not allowed).
694     *
695     * @see #getTickLabelFont()
696     */
697    public void setTickLabelFont(Font font) {
698        Args.nullNotPermitted(font, "font");
699        if (!this.tickLabelFont.equals(font)) {
700            this.tickLabelFont = font;
701            fireChangeEvent();
702        }
703    }
704
705    /**
706     * Returns the color/shade used for the tick labels.
707     *
708     * @return The paint used for the tick labels.
709     *
710     * @see #setTickLabelPaint(Paint)
711     */
712    public Paint getTickLabelPaint() {
713        return this.tickLabelPaint;
714    }
715
716    /**
717     * Sets the paint used to draw tick labels (if they are showing) and
718     * sends an {@link AxisChangeEvent} to all registered listeners.
719     *
720     * @param paint  the paint ({@code null} not permitted).
721     *
722     * @see #getTickLabelPaint()
723     */
724    public void setTickLabelPaint(Paint paint) {
725        Args.nullNotPermitted(paint, "paint");
726        this.tickLabelPaint = paint;
727        fireChangeEvent();
728    }
729
730    /**
731     * Returns the insets for the tick labels.
732     *
733     * @return The insets (never {@code null}).
734     *
735     * @see #setTickLabelInsets(RectangleInsets)
736     */
737    public RectangleInsets getTickLabelInsets() {
738        return this.tickLabelInsets;
739    }
740
741    /**
742     * Sets the insets for the tick labels and sends an {@link AxisChangeEvent}
743     * to all registered listeners.
744     *
745     * @param insets  the insets ({@code null} not permitted).
746     *
747     * @see #getTickLabelInsets()
748     */
749    public void setTickLabelInsets(RectangleInsets insets) {
750        Args.nullNotPermitted(insets, "insets");
751        if (!this.tickLabelInsets.equals(insets)) {
752            this.tickLabelInsets = insets;
753            fireChangeEvent();
754        }
755    }
756
757    /**
758     * Returns the flag that indicates whether or not the tick marks are
759     * showing.
760     *
761     * @return The flag that indicates whether or not the tick marks are
762     *         showing.
763     *
764     * @see #setTickMarksVisible(boolean)
765     */
766    public boolean isTickMarksVisible() {
767        return this.tickMarksVisible;
768    }
769
770    /**
771     * Sets the flag that indicates whether or not the tick marks are showing
772     * and sends an {@link AxisChangeEvent} to all registered listeners.
773     *
774     * @param flag  the flag.
775     *
776     * @see #isTickMarksVisible()
777     */
778    public void setTickMarksVisible(boolean flag) {
779        if (flag != this.tickMarksVisible) {
780            this.tickMarksVisible = flag;
781            fireChangeEvent();
782        }
783    }
784
785    /**
786     * Returns the inside length of the tick marks.
787     *
788     * @return The length.
789     *
790     * @see #getTickMarkOutsideLength()
791     * @see #setTickMarkInsideLength(float)
792     */
793    public float getTickMarkInsideLength() {
794        return this.tickMarkInsideLength;
795    }
796
797    /**
798     * Sets the inside length of the tick marks and sends
799     * an {@link AxisChangeEvent} to all registered listeners.
800     *
801     * @param length  the new length.
802     *
803     * @see #getTickMarkInsideLength()
804     */
805    public void setTickMarkInsideLength(float length) {
806        this.tickMarkInsideLength = length;
807        fireChangeEvent();
808    }
809
810    /**
811     * Returns the outside length of the tick marks.
812     *
813     * @return The length.
814     *
815     * @see #getTickMarkInsideLength()
816     * @see #setTickMarkOutsideLength(float)
817     */
818    public float getTickMarkOutsideLength() {
819        return this.tickMarkOutsideLength;
820    }
821
822    /**
823     * Sets the outside length of the tick marks and sends
824     * an {@link AxisChangeEvent} to all registered listeners.
825     *
826     * @param length  the new length.
827     *
828     * @see #getTickMarkInsideLength()
829     */
830    public void setTickMarkOutsideLength(float length) {
831        this.tickMarkOutsideLength = length;
832        fireChangeEvent();
833    }
834
835    /**
836     * Returns the stroke used to draw tick marks.
837     *
838     * @return The stroke (never {@code null}).
839     *
840     * @see #setTickMarkStroke(Stroke)
841     */
842    public Stroke getTickMarkStroke() {
843        return this.tickMarkStroke;
844    }
845
846    /**
847     * Sets the stroke used to draw tick marks and sends
848     * an {@link AxisChangeEvent} to all registered listeners.
849     *
850     * @param stroke  the stroke ({@code null} not permitted).
851     *
852     * @see #getTickMarkStroke()
853     */
854    public void setTickMarkStroke(Stroke stroke) {
855        Args.nullNotPermitted(stroke, "stroke");
856        if (!this.tickMarkStroke.equals(stroke)) {
857            this.tickMarkStroke = stroke;
858            fireChangeEvent();
859        }
860    }
861
862    /**
863     * Returns the paint used to draw tick marks (if they are showing).
864     *
865     * @return The paint (never {@code null}).
866     *
867     * @see #setTickMarkPaint(Paint)
868     */
869    public Paint getTickMarkPaint() {
870        return this.tickMarkPaint;
871    }
872
873    /**
874     * Sets the paint used to draw tick marks and sends an
875     * {@link AxisChangeEvent} to all registered listeners.
876     *
877     * @param paint  the paint ({@code null} not permitted).
878     *
879     * @see #getTickMarkPaint()
880     */
881    public void setTickMarkPaint(Paint paint) {
882        Args.nullNotPermitted(paint, "paint");
883        this.tickMarkPaint = paint;
884        fireChangeEvent();
885    }
886
887    /**
888     * Returns the inside length of the minor tick marks.
889     *
890     * @return The length.
891     *
892     * @see #getMinorTickMarkOutsideLength()
893     * @see #setMinorTickMarkInsideLength(float)
894     */
895    public float getMinorTickMarkInsideLength() {
896        return this.minorTickMarkInsideLength;
897    }
898
899    /**
900     * Sets the inside length of the minor tick marks and sends
901     * an {@link AxisChangeEvent} to all registered listeners.
902     *
903     * @param length  the new length.
904     *
905     * @see #getMinorTickMarkInsideLength()
906     */
907    public void setMinorTickMarkInsideLength(float length) {
908        this.minorTickMarkInsideLength = length;
909        fireChangeEvent();
910    }
911
912    /**
913     * Returns the outside length of the minor tick marks.
914     *
915     * @return The length.
916     *
917     * @see #getMinorTickMarkInsideLength()
918     * @see #setMinorTickMarkOutsideLength(float)
919     */
920    public float getMinorTickMarkOutsideLength() {
921        return this.minorTickMarkOutsideLength;
922    }
923
924    /**
925     * Sets the outside length of the minor tick marks and sends
926     * an {@link AxisChangeEvent} to all registered listeners.
927     *
928     * @param length  the new length.
929     *
930     * @see #getMinorTickMarkInsideLength()
931     */
932    public void setMinorTickMarkOutsideLength(float length) {
933        this.minorTickMarkOutsideLength = length;
934        fireChangeEvent();
935    }
936
937    /**
938     * Returns the plot that the axis is assigned to.  This method will return
939     * {@code null} if the axis is not currently assigned to a plot.
940     *
941     * @return The plot that the axis is assigned to (possibly {@code null}).
942     *
943     * @see #setPlot(Plot)
944     */
945    public Plot getPlot() {
946        return this.plot;
947    }
948
949    /**
950     * Sets a reference to the plot that the axis is assigned to.
951     * <P>
952     * This method is used internally, you shouldn't need to call it yourself.
953     *
954     * @param plot  the plot.
955     *
956     * @see #getPlot()
957     */
958    public void setPlot(Plot plot) {
959        this.plot = plot;
960        configure();
961    }
962
963    /**
964     * Returns the fixed dimension for the axis.
965     *
966     * @return The fixed dimension.
967     *
968     * @see #setFixedDimension(double)
969     */
970    public double getFixedDimension() {
971        return this.fixedDimension;
972    }
973
974    /**
975     * Sets the fixed dimension for the axis.
976     * <P>
977     * This is used when combining more than one plot on a chart.  In this case,
978     * there may be several axes that need to have the same height or width so
979     * that they are aligned.  This method is used to fix a dimension for the
980     * axis (the context determines whether the dimension is horizontal or
981     * vertical).
982     *
983     * @param dimension  the fixed dimension.
984     *
985     * @see #getFixedDimension()
986     */
987    public void setFixedDimension(double dimension) {
988        this.fixedDimension = dimension;
989    }
990
991    /**
992     * Configures the axis to work with the current plot.  Override this method
993     * to perform any special processing (such as auto-rescaling).
994     */
995    public abstract void configure();
996
997    /**
998     * Estimates the space (height or width) required to draw the axis.
999     *
1000     * @param g2  the graphics device.
1001     * @param plot  the plot that the axis belongs to.
1002     * @param plotArea  the area within which the plot (including axes) should
1003     *                  be drawn.
1004     * @param edge  the axis location.
1005     * @param space  space already reserved.
1006     *
1007     * @return The space required to draw the axis (including pre-reserved
1008     *         space).
1009     */
1010    public abstract AxisSpace reserveSpace(Graphics2D g2, Plot plot,
1011                                           Rectangle2D plotArea,
1012                                           RectangleEdge edge,
1013                                           AxisSpace space);
1014
1015    /**
1016     * Draws the axis on a Java 2D graphics device (such as the screen or a
1017     * printer).
1018     *
1019     * @param g2  the graphics device ({@code null} not permitted).
1020     * @param cursor  the cursor location (determines where to draw the axis).
1021     * @param plotArea  the area within which the axes and plot should be drawn.
1022     * @param dataArea  the area within which the data should be drawn.
1023     * @param edge  the axis location ({@code null} not permitted).
1024     * @param plotState  collects information about the plot
1025     *                   ({@code null} permitted).
1026     *
1027     * @return The axis state (never {@code null}).
1028     */
1029    public abstract AxisState draw(Graphics2D g2, double cursor,
1030            Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge,
1031            PlotRenderingInfo plotState);
1032
1033    /**
1034     * Calculates the positions of the ticks for the axis, storing the results
1035     * in the tick list (ready for drawing).
1036     *
1037     * @param g2  the graphics device.
1038     * @param state  the axis state.
1039     * @param dataArea  the area inside the axes.
1040     * @param edge  the edge on which the axis is located.
1041     *
1042     * @return The list of ticks.
1043     */
1044    public abstract List refreshTicks(Graphics2D g2, AxisState state,
1045            Rectangle2D dataArea, RectangleEdge edge);
1046
1047    /**
1048     * Creates an entity for the axis and adds it to the rendering info.
1049     * If {@code plotState} is {@code null}, this means that rendering info
1050     * is not being collected so this method simply returns without doing 
1051     * anything.
1052     * 
1053     * @param cursor  the initial cursor value.
1054     * @param state  the axis state after completion of the drawing with a
1055     *     possibly updated cursor position.
1056     * @param dataArea  the data area.
1057     * @param edge  the edge ({@code null} not permitted).
1058     * @param plotState  the PlotRenderingInfo from which a reference to the
1059     *     entity collection can be obtained ({@code null} permitted).
1060     */
1061    protected void createAndAddEntity(double cursor, AxisState state,
1062            Rectangle2D dataArea, RectangleEdge edge,
1063            PlotRenderingInfo plotState) {
1064
1065        Args.nullNotPermitted(edge, "edge");
1066        if (plotState == null || plotState.getOwner() == null) {
1067            return;  // no need to create entity if we can't save it anyways...
1068        }
1069        Rectangle2D hotspot = null;
1070        if (edge.equals(RectangleEdge.TOP)) {
1071            hotspot = new Rectangle2D.Double(dataArea.getX(),
1072                    state.getCursor(), dataArea.getWidth(),
1073                    cursor - state.getCursor());
1074        }
1075        else if (edge.equals(RectangleEdge.BOTTOM)) {
1076            hotspot = new Rectangle2D.Double(dataArea.getX(), cursor,
1077                    dataArea.getWidth(), state.getCursor() - cursor);
1078        }
1079        else if (edge.equals(RectangleEdge.LEFT)) {
1080            hotspot = new Rectangle2D.Double(state.getCursor(),
1081                    dataArea.getY(), cursor - state.getCursor(),
1082                    dataArea.getHeight());
1083        }
1084        else if (edge.equals(RectangleEdge.RIGHT)) {
1085            hotspot = new Rectangle2D.Double(cursor, dataArea.getY(),
1086                    state.getCursor() - cursor, dataArea.getHeight());
1087        }
1088        EntityCollection e = plotState.getOwner().getEntityCollection();
1089        if (e != null) {
1090            e.add(new AxisEntity(hotspot, this));
1091        }
1092    }
1093
1094    /**
1095     * Registers an object for notification of changes to the axis.
1096     *
1097     * @param listener  the object that is being registered.
1098     *
1099     * @see #removeChangeListener(AxisChangeListener)
1100     */
1101    public void addChangeListener(AxisChangeListener listener) {
1102        this.listenerList.add(AxisChangeListener.class, listener);
1103    }
1104
1105    /**
1106     * Deregisters an object for notification of changes to the axis.
1107     *
1108     * @param listener  the object to deregister.
1109     *
1110     * @see #addChangeListener(AxisChangeListener)
1111     */
1112    public void removeChangeListener(AxisChangeListener listener) {
1113        this.listenerList.remove(AxisChangeListener.class, listener);
1114    }
1115
1116    /**
1117     * Returns {@code true} if the specified object is registered with
1118     * the dataset as a listener.  Most applications won't need to call this
1119     * method, it exists mainly for use by unit testing code.
1120     *
1121     * @param listener  the listener.
1122     *
1123     * @return A boolean.
1124     */
1125    public boolean hasListener(EventListener listener) {
1126        List list = Arrays.asList(this.listenerList.getListenerList());
1127        return list.contains(listener);
1128    }
1129
1130    /**
1131     * Notifies all registered listeners that the axis has changed.
1132     * The AxisChangeEvent provides information about the change.
1133     *
1134     * @param event  information about the change to the axis.
1135     */
1136    protected void notifyListeners(AxisChangeEvent event) {
1137        Object[] listeners = this.listenerList.getListenerList();
1138        for (int i = listeners.length - 2; i >= 0; i -= 2) {
1139            if (listeners[i] == AxisChangeListener.class) {
1140                ((AxisChangeListener) listeners[i + 1]).axisChanged(event);
1141            }
1142        }
1143    }
1144
1145    /**
1146     * Sends an {@link AxisChangeEvent} to all registered listeners.
1147     */
1148    protected void fireChangeEvent() {
1149        notifyListeners(new AxisChangeEvent(this));
1150    }
1151
1152    /**
1153     * Returns a rectangle that encloses the axis label.  This is typically
1154     * used for layout purposes (it gives the maximum dimensions of the label).
1155     *
1156     * @param g2  the graphics device.
1157     * @param edge  the edge of the plot area along which the axis is measuring.
1158     *
1159     * @return The enclosing rectangle.
1160     */
1161    protected Rectangle2D getLabelEnclosure(Graphics2D g2, RectangleEdge edge) {
1162        Rectangle2D result = new Rectangle2D.Double();
1163        Rectangle2D bounds = null;
1164        if (this.attributedLabel != null) {
1165            TextLayout layout = new TextLayout(
1166                    this.attributedLabel.getIterator(), 
1167                    g2.getFontRenderContext());
1168            bounds = layout.getBounds();
1169        } else {
1170            String axisLabel = getLabel();
1171            if (axisLabel != null && !axisLabel.equals("")) {
1172                FontMetrics fm = g2.getFontMetrics(getLabelFont());
1173                bounds = TextUtils.getTextBounds(axisLabel, g2, fm);
1174            }
1175        }
1176        if (bounds != null) {
1177            RectangleInsets insets = getLabelInsets();
1178            bounds = insets.createOutsetRectangle(bounds);
1179            double angle = getLabelAngle();
1180            if (edge == RectangleEdge.LEFT || edge == RectangleEdge.RIGHT) {
1181                angle = angle - Math.PI / 2.0;
1182            }
1183            double x = bounds.getCenterX();
1184            double y = bounds.getCenterY();
1185            AffineTransform transformer
1186                = AffineTransform.getRotateInstance(angle, x, y);
1187            Shape labelBounds = transformer.createTransformedShape(bounds);
1188            result = labelBounds.getBounds2D();
1189        }
1190        return result;
1191    }
1192
1193    /**
1194     * Returns the x-coordinate for the point to which the axis label should 
1195     * be aligned.
1196     * 
1197     * @param location  the axis label location ({@code null} not permitted).
1198     * @param dataArea  the display area in which the data will be rendered ({@code null} not permitted).
1199     * 
1200     * @return The x-coordinate. 
1201     */
1202    protected double labelLocationX(AxisLabelLocation location, 
1203            Rectangle2D dataArea) {
1204        if (location.equals(AxisLabelLocation.HIGH_END)) {
1205            return dataArea.getMaxX();
1206        }
1207        if (location.equals(AxisLabelLocation.MIDDLE)) {
1208            return dataArea.getCenterX();
1209        }
1210        if (location.equals(AxisLabelLocation.LOW_END)) {
1211            return dataArea.getMinX();
1212        }
1213        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1214    }
1215    
1216    /**
1217     * Returns the y-coordinate for the point to which the axis label should
1218     * be aligned.
1219     * 
1220     * @param location  the location ({@code null} not permitted).
1221     * @param dataArea  the data area ({@code null} not permitted).
1222     * 
1223     * @return The y-coordinate. 
1224     */
1225    protected double labelLocationY(AxisLabelLocation location, 
1226            Rectangle2D dataArea) {
1227        if (location.equals(AxisLabelLocation.HIGH_END)) {
1228            return dataArea.getMinY();
1229        }
1230        if (location.equals(AxisLabelLocation.MIDDLE)) {
1231            return dataArea.getCenterY();
1232        }
1233        if (location.equals(AxisLabelLocation.LOW_END)) {
1234            return dataArea.getMaxY();
1235        }
1236        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1237    }
1238    
1239    /**
1240     * Returns the appropriate horizontal text anchor for the specified axis 
1241     * location.
1242     * 
1243     * @param location  the location ({@code null} not permitted).
1244     * 
1245     * @return The text anchor (never {@code null}). 
1246     */
1247    protected TextAnchor labelAnchorH(AxisLabelLocation location) {
1248        if (location.equals(AxisLabelLocation.HIGH_END)) {
1249            return TextAnchor.CENTER_RIGHT;
1250        }
1251        if (location.equals(AxisLabelLocation.MIDDLE)) {
1252            return TextAnchor.CENTER;
1253        }
1254        if (location.equals(AxisLabelLocation.LOW_END)) {
1255            return TextAnchor.CENTER_LEFT;
1256        }
1257        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1258    }
1259    
1260    /**
1261     * Returns the appropriate vertical text anchor for the specified axis 
1262     * location.
1263     * 
1264     * @param location  the location ({@code null} not permitted).
1265     * 
1266     * @return The text anchor (never {@code null}). 
1267     */
1268    protected TextAnchor labelAnchorV(AxisLabelLocation location) {
1269        if (location.equals(AxisLabelLocation.HIGH_END)) {
1270            return TextAnchor.CENTER_RIGHT;
1271        }
1272        if (location.equals(AxisLabelLocation.MIDDLE)) {
1273            return TextAnchor.CENTER;
1274        }
1275        if (location.equals(AxisLabelLocation.LOW_END)) {
1276            return TextAnchor.CENTER_LEFT;
1277        }
1278        throw new RuntimeException("Unexpected AxisLabelLocation: " + location);
1279    }
1280
1281    /**
1282     * Draws the axis label.
1283     *
1284     * @param label  the label text.
1285     * @param g2  the graphics device.
1286     * @param plotArea  the plot area.
1287     * @param dataArea  the area inside the axes.
1288     * @param edge  the location of the axis.
1289     * @param state  the axis state ({@code null} not permitted).
1290     *
1291     * @return Information about the axis.
1292     */
1293    protected AxisState drawLabel(String label, Graphics2D g2,
1294            Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge,
1295            AxisState state) {
1296
1297        // it is unlikely that 'state' will be null, but check anyway...
1298        Args.nullNotPermitted(state, "state");
1299
1300        if ((label == null) || (label.equals(""))) {
1301            return state;
1302        }
1303
1304        Font font = getLabelFont();
1305        RectangleInsets insets = getLabelInsets();
1306        g2.setFont(font);
1307        g2.setPaint(getLabelPaint());
1308        FontMetrics fm = g2.getFontMetrics();
1309        Rectangle2D labelBounds = TextUtils.getTextBounds(label, g2, fm);
1310
1311        if (edge == RectangleEdge.TOP) {
1312            AffineTransform t = AffineTransform.getRotateInstance(
1313                    getLabelAngle(), labelBounds.getCenterX(),
1314                    labelBounds.getCenterY());
1315            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1316            labelBounds = rotatedLabelBounds.getBounds2D();
1317            double labelx = labelLocationX(this.labelLocation, dataArea);
1318            double labely = state.getCursor() - insets.getBottom()
1319                            - labelBounds.getHeight() / 2.0;
1320            TextAnchor anchor = labelAnchorH(this.labelLocation);
1321            TextUtils.drawRotatedString(label, g2, (float) labelx,
1322                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1323            state.cursorUp(insets.getTop() + labelBounds.getHeight()
1324                    + insets.getBottom());
1325        }
1326        else if (edge == RectangleEdge.BOTTOM) {
1327            AffineTransform t = AffineTransform.getRotateInstance(
1328                    getLabelAngle(), labelBounds.getCenterX(),
1329                    labelBounds.getCenterY());
1330            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1331            labelBounds = rotatedLabelBounds.getBounds2D();
1332            double labelx = labelLocationX(this.labelLocation, dataArea);
1333            double labely = state.getCursor()
1334                            + insets.getTop() + labelBounds.getHeight() / 2.0;
1335            TextAnchor anchor = labelAnchorH(this.labelLocation);
1336            TextUtils.drawRotatedString(label, g2, (float) labelx,
1337                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1338            state.cursorDown(insets.getTop() + labelBounds.getHeight()
1339                    + insets.getBottom());
1340        }
1341        else if (edge == RectangleEdge.LEFT) {
1342            AffineTransform t = AffineTransform.getRotateInstance(
1343                    getLabelAngle() - Math.PI / 2.0, labelBounds.getCenterX(),
1344                    labelBounds.getCenterY());
1345            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1346            labelBounds = rotatedLabelBounds.getBounds2D();
1347            double labelx = state.getCursor()
1348                            - insets.getRight() - labelBounds.getWidth() / 2.0;
1349            double labely = labelLocationY(this.labelLocation, dataArea);
1350            TextAnchor anchor = labelAnchorV(this.labelLocation);
1351            TextUtils.drawRotatedString(label, g2, (float) labelx,
1352                    (float) labely, anchor, getLabelAngle() - Math.PI / 2.0, 
1353                    anchor);
1354            state.cursorLeft(insets.getLeft() + labelBounds.getWidth()
1355                    + insets.getRight());
1356        }
1357        else if (edge == RectangleEdge.RIGHT) {
1358            AffineTransform t = AffineTransform.getRotateInstance(
1359                    getLabelAngle() + Math.PI / 2.0,
1360                    labelBounds.getCenterX(), labelBounds.getCenterY());
1361            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1362            labelBounds = rotatedLabelBounds.getBounds2D();
1363            double labelx = state.getCursor()
1364                            + insets.getLeft() + labelBounds.getWidth() / 2.0;
1365            double labely = labelLocationY(this.labelLocation, dataArea);
1366            TextAnchor anchor = labelAnchorV(this.labelLocation);
1367            TextUtils.drawRotatedString(label, g2, (float) labelx,
1368                    (float) labely, anchor, getLabelAngle() + Math.PI / 2.0, 
1369                    anchor);
1370            state.cursorRight(insets.getLeft() + labelBounds.getWidth()
1371                    + insets.getRight());
1372        }
1373
1374        return state;
1375
1376    }
1377
1378    /**
1379     * Draws the axis label.
1380     *
1381     * @param label  the label text.
1382     * @param g2  the graphics device.
1383     * @param plotArea  the plot area.
1384     * @param dataArea  the area inside the axes.
1385     * @param edge  the location of the axis.
1386     * @param state  the axis state ({@code null} not permitted).
1387     *
1388     * @return Information about the axis.
1389     */
1390    protected AxisState drawAttributedLabel(AttributedString label, 
1391            Graphics2D g2, Rectangle2D plotArea, Rectangle2D dataArea, 
1392            RectangleEdge edge, AxisState state) {
1393
1394        // it is unlikely that 'state' will be null, but check anyway...
1395        Args.nullNotPermitted(state, "state");
1396
1397        if (label == null) {
1398            return state;
1399        }
1400
1401        RectangleInsets insets = getLabelInsets();
1402        g2.setFont(getLabelFont());
1403        g2.setPaint(getLabelPaint());
1404        TextLayout layout = new TextLayout(this.attributedLabel.getIterator(),
1405                g2.getFontRenderContext());
1406        Rectangle2D labelBounds = layout.getBounds();
1407
1408        if (edge == RectangleEdge.TOP) {
1409            AffineTransform t = AffineTransform.getRotateInstance(
1410                    getLabelAngle(), labelBounds.getCenterX(),
1411                    labelBounds.getCenterY());
1412            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1413            labelBounds = rotatedLabelBounds.getBounds2D();
1414            double labelx = labelLocationX(this.labelLocation, dataArea);
1415            double labely = state.getCursor() - insets.getBottom()
1416                            - labelBounds.getHeight() / 2.0;
1417            TextAnchor anchor = labelAnchorH(this.labelLocation);
1418            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1419                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1420            state.cursorUp(insets.getTop() + labelBounds.getHeight()
1421                    + insets.getBottom());
1422        }
1423        else if (edge == RectangleEdge.BOTTOM) {
1424            AffineTransform t = AffineTransform.getRotateInstance(
1425                    getLabelAngle(), labelBounds.getCenterX(),
1426                    labelBounds.getCenterY());
1427            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1428            labelBounds = rotatedLabelBounds.getBounds2D();
1429            double labelx = labelLocationX(this.labelLocation, dataArea);
1430            double labely = state.getCursor()
1431                            + insets.getTop() + labelBounds.getHeight() / 2.0;
1432            TextAnchor anchor = labelAnchorH(this.labelLocation);
1433            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1434                    (float) labely, anchor, getLabelAngle(), TextAnchor.CENTER);
1435            state.cursorDown(insets.getTop() + labelBounds.getHeight()
1436                    + insets.getBottom());
1437        }
1438        else if (edge == RectangleEdge.LEFT) {
1439            AffineTransform t = AffineTransform.getRotateInstance(
1440                    getLabelAngle() - Math.PI / 2.0, labelBounds.getCenterX(),
1441                    labelBounds.getCenterY());
1442            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1443            labelBounds = rotatedLabelBounds.getBounds2D();
1444            double labelx = state.getCursor()
1445                            - insets.getRight() - labelBounds.getWidth() / 2.0;
1446            double labely = labelLocationY(this.labelLocation, dataArea);
1447            TextAnchor anchor = labelAnchorV(this.labelLocation);
1448            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1449                    (float) labely, anchor, getLabelAngle() - Math.PI / 2.0, 
1450                    anchor);
1451            state.cursorLeft(insets.getLeft() + labelBounds.getWidth()
1452                    + insets.getRight());
1453        }
1454        else if (edge == RectangleEdge.RIGHT) {
1455            AffineTransform t = AffineTransform.getRotateInstance(
1456                    getLabelAngle() + Math.PI / 2.0,
1457                    labelBounds.getCenterX(), labelBounds.getCenterY());
1458            Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
1459            labelBounds = rotatedLabelBounds.getBounds2D();
1460            double labelx = state.getCursor()
1461                            + insets.getLeft() + labelBounds.getWidth() / 2.0;
1462            double labely = labelLocationY(this.labelLocation, dataArea);
1463            TextAnchor anchor = labelAnchorV(this.labelLocation);
1464            AttrStringUtils.drawRotatedString(label, g2, (float) labelx,
1465                    (float) labely, anchor, getLabelAngle() + Math.PI / 2.0, 
1466                    anchor);
1467            state.cursorRight(insets.getLeft() + labelBounds.getWidth()
1468                    + insets.getRight());
1469        }
1470        return state;
1471    }
1472
1473    /**
1474     * Draws an axis line at the current cursor position and edge.
1475     *
1476     * @param g2  the graphics device.
1477     * @param cursor  the cursor position.
1478     * @param dataArea  the data area.
1479     * @param edge  the edge.
1480     */
1481    protected void drawAxisLine(Graphics2D g2, double cursor,
1482            Rectangle2D dataArea, RectangleEdge edge) {
1483        Line2D axisLine = null;
1484        double x = dataArea.getX();
1485        double y = dataArea.getY();
1486        if (edge == RectangleEdge.TOP) {
1487            axisLine = new Line2D.Double(x, cursor, dataArea.getMaxX(), cursor);
1488        } else if (edge == RectangleEdge.BOTTOM) {
1489            axisLine = new Line2D.Double(x, cursor, dataArea.getMaxX(), cursor);
1490        } else if (edge == RectangleEdge.LEFT) {
1491            axisLine = new Line2D.Double(cursor, y, cursor, dataArea.getMaxY());
1492        } else if (edge == RectangleEdge.RIGHT) {
1493            axisLine = new Line2D.Double(cursor, y, cursor, dataArea.getMaxY());
1494        }
1495        g2.setPaint(this.axisLinePaint);
1496        g2.setStroke(this.axisLineStroke);
1497        Object saved = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
1498        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
1499                RenderingHints.VALUE_STROKE_NORMALIZE);
1500        g2.draw(axisLine);
1501        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, saved);
1502    }
1503
1504    /**
1505     * Returns a clone of the axis.
1506     *
1507     * @return A clone.
1508     *
1509     * @throws CloneNotSupportedException if some component of the axis does
1510     *         not support cloning.
1511     */
1512    @Override
1513    public Object clone() throws CloneNotSupportedException {
1514        Axis clone = (Axis) super.clone();
1515        // It's up to the plot which clones up to restore the correct references
1516        clone.plot = null;
1517        clone.listenerList = new EventListenerList();
1518        return clone;
1519    }
1520
1521    /**
1522     * Tests this axis for equality with another object.
1523     *
1524     * @param obj  the object ({@code null} permitted).
1525     *
1526     * @return {@code true} or {@code false}.
1527     */
1528    @Override
1529    public boolean equals(Object obj) {
1530        if (obj == this) {
1531            return true;
1532        }
1533        if (!(obj instanceof Axis)) {
1534            return false;
1535        }
1536        Axis that = (Axis) obj;
1537        if (this.visible != that.visible) {
1538            return false;
1539        }
1540        if (!Objects.equals(this.label, that.label)) {
1541            return false;
1542        }
1543        if (!AttributedStringUtils.equal(this.attributedLabel, 
1544                that.attributedLabel)) {
1545            return false;
1546        }
1547        if (!Objects.equals(this.labelFont, that.labelFont)) {
1548            return false;
1549        }
1550        if (!PaintUtils.equal(this.labelPaint, that.labelPaint)) {
1551            return false;
1552        }
1553        if (!Objects.equals(this.labelInsets, that.labelInsets)) {
1554            return false;
1555        }
1556        if (this.labelAngle != that.labelAngle) {
1557            return false;
1558        }
1559        if (!this.labelLocation.equals(that.labelLocation)) {
1560            return false;
1561        }
1562        if (this.axisLineVisible != that.axisLineVisible) {
1563            return false;
1564        }
1565        if (!Objects.equals(this.axisLineStroke, that.axisLineStroke)) {
1566            return false;
1567        }
1568        if (!PaintUtils.equal(this.axisLinePaint, that.axisLinePaint)) {
1569            return false;
1570        }
1571        if (this.tickLabelsVisible != that.tickLabelsVisible) {
1572            return false;
1573        }
1574        if (!Objects.equals(this.tickLabelFont, that.tickLabelFont)) {
1575            return false;
1576        }
1577        if (!PaintUtils.equal(this.tickLabelPaint, that.tickLabelPaint)) {
1578            return false;
1579        }
1580        if (!Objects.equals(this.tickLabelInsets, that.tickLabelInsets)) {
1581            return false;
1582        }
1583        if (this.tickMarksVisible != that.tickMarksVisible) {
1584            return false;
1585        }
1586        if (this.tickMarkInsideLength != that.tickMarkInsideLength) {
1587            return false;
1588        }
1589        if (this.tickMarkOutsideLength != that.tickMarkOutsideLength) {
1590            return false;
1591        }
1592        if (!PaintUtils.equal(this.tickMarkPaint, that.tickMarkPaint)) {
1593            return false;
1594        }
1595        if (!Objects.equals(this.tickMarkStroke, that.tickMarkStroke)) {
1596            return false;
1597        }
1598        if (this.minorTickMarksVisible != that.minorTickMarksVisible) {
1599            return false;
1600        }
1601        if (this.minorTickMarkInsideLength != that.minorTickMarkInsideLength) {
1602            return false;
1603        }
1604        if (this.minorTickMarkOutsideLength
1605                != that.minorTickMarkOutsideLength) {
1606            return false;
1607        }
1608        if (this.fixedDimension != that.fixedDimension) {
1609            return false;
1610        }
1611        return true;
1612    }
1613
1614    /**
1615     * Returns a hash code for this instance.
1616     * 
1617     * @return A hash code. 
1618     */
1619    @Override
1620    public int hashCode() {
1621        int hash = 3;
1622        if (this.label != null) {
1623            hash = 83 * hash + this.label.hashCode();
1624        }
1625        return hash;
1626    }
1627
1628    /**
1629     * Provides serialization support.
1630     *
1631     * @param stream  the output stream.
1632     *
1633     * @throws IOException  if there is an I/O error.
1634     */
1635    private void writeObject(ObjectOutputStream stream) throws IOException {
1636        stream.defaultWriteObject();
1637        SerialUtils.writeAttributedString(this.attributedLabel, stream);
1638        SerialUtils.writePaint(this.labelPaint, stream);
1639        SerialUtils.writePaint(this.tickLabelPaint, stream);
1640        SerialUtils.writeStroke(this.axisLineStroke, stream);
1641        SerialUtils.writePaint(this.axisLinePaint, stream);
1642        SerialUtils.writeStroke(this.tickMarkStroke, stream);
1643        SerialUtils.writePaint(this.tickMarkPaint, stream);
1644    }
1645
1646    /**
1647     * Provides serialization support.
1648     *
1649     * @param stream  the input stream.
1650     *
1651     * @throws IOException  if there is an I/O error.
1652     * @throws ClassNotFoundException  if there is a classpath problem.
1653     */
1654    private void readObject(ObjectInputStream stream)
1655        throws IOException, ClassNotFoundException {
1656        stream.defaultReadObject();
1657        this.attributedLabel = SerialUtils.readAttributedString(stream);
1658        this.labelPaint = SerialUtils.readPaint(stream);
1659        this.tickLabelPaint = SerialUtils.readPaint(stream);
1660        this.axisLineStroke = SerialUtils.readStroke(stream);
1661        this.axisLinePaint = SerialUtils.readPaint(stream);
1662        this.tickMarkStroke = SerialUtils.readStroke(stream);
1663        this.tickMarkPaint = SerialUtils.readPaint(stream);
1664        this.listenerList = new EventListenerList();
1665    }
1666
1667}