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