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 * CategoryAxis.java
029 * -----------------
030 * (C) Copyright 2000-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   Pady Srinivasan (patch 1217634);
034 *                   Peter Kolb (patches 2497611 and 2603321);
035 *
036 */
037
038package org.jfree.chart.axis;
039
040import java.awt.Font;
041import java.awt.Graphics2D;
042import java.awt.Paint;
043import java.awt.RenderingHints;
044import java.awt.Shape;
045import java.awt.geom.Line2D;
046import java.awt.geom.Point2D;
047import java.awt.geom.Rectangle2D;
048import java.io.IOException;
049import java.io.ObjectInputStream;
050import java.io.ObjectOutputStream;
051import java.io.Serializable;
052import java.util.HashMap;
053import java.util.List;
054import java.util.Map;
055import java.util.Objects;
056import java.util.Set;
057
058import org.jfree.chart.entity.CategoryLabelEntity;
059import org.jfree.chart.entity.EntityCollection;
060import org.jfree.chart.event.AxisChangeEvent;
061import org.jfree.chart.plot.CategoryPlot;
062import org.jfree.chart.plot.Plot;
063import org.jfree.chart.plot.PlotRenderingInfo;
064import org.jfree.chart.text.G2TextMeasurer;
065import org.jfree.chart.text.TextBlock;
066import org.jfree.chart.text.TextUtils;
067import org.jfree.chart.api.RectangleEdge;
068import org.jfree.chart.api.RectangleInsets;
069import org.jfree.chart.block.Size2D;
070import org.jfree.chart.internal.Args;
071import org.jfree.chart.internal.PaintUtils;
072import org.jfree.chart.internal.SerialUtils;
073import org.jfree.chart.internal.ShapeUtils;
074import org.jfree.data.category.CategoryDataset;
075
076/**
077 * An axis that displays categories.
078 */
079public class CategoryAxis extends Axis implements Cloneable, Serializable {
080
081    /** For serialization. */
082    private static final long serialVersionUID = 5886554608114265863L;
083
084    /**
085     * The default margin for the axis (used for both lower and upper margins).
086     */
087    public static final double DEFAULT_AXIS_MARGIN = 0.05;
088
089    /**
090     * The default margin between categories (a percentage of the overall axis
091     * length).
092     */
093    public static final double DEFAULT_CATEGORY_MARGIN = 0.20;
094
095    /** The amount of space reserved at the start of the axis. */
096    private double lowerMargin;
097
098    /** The amount of space reserved at the end of the axis. */
099    private double upperMargin;
100
101    /** The amount of space reserved between categories. */
102    private double categoryMargin;
103
104    /** The maximum number of lines for category labels. */
105    private int maximumCategoryLabelLines;
106
107    /**
108     * A ratio that is multiplied by the width of one category to determine the
109     * maximum label width.
110     */
111    private float maximumCategoryLabelWidthRatio;
112
113    /** The category label offset. */
114    private int categoryLabelPositionOffset;
115
116    /**
117     * A structure defining the category label positions for each axis
118     * location.
119     */
120    private CategoryLabelPositions categoryLabelPositions;
121
122    /** Storage for tick label font overrides (if any). */
123    private Map<Comparable, Font> tickLabelFontMap; 
124
125    /** Storage for tick label paint overrides (if any). */
126    private transient Map<Comparable, Paint> tickLabelPaintMap;
127
128    /** Storage for the category label tooltips (if any). */
129    private Map<Comparable, String> categoryLabelToolTips;
130
131    /** Storage for the category label URLs (if any). */
132    private Map<Comparable, String> categoryLabelURLs;
133    
134    /**
135     * Creates a new category axis with no label.
136     */
137    public CategoryAxis() {
138        this(null);
139    }
140
141    /**
142     * Constructs a category axis, using default values where necessary.
143     *
144     * @param label  the axis label ({@code null} permitted).
145     */
146    public CategoryAxis(String label) {
147        super(label);
148
149        this.lowerMargin = DEFAULT_AXIS_MARGIN;
150        this.upperMargin = DEFAULT_AXIS_MARGIN;
151        this.categoryMargin = DEFAULT_CATEGORY_MARGIN;
152        this.maximumCategoryLabelLines = 1;
153        this.maximumCategoryLabelWidthRatio = 0.0f;
154
155        this.categoryLabelPositionOffset = 4;
156        this.categoryLabelPositions = CategoryLabelPositions.STANDARD;
157        this.tickLabelFontMap = new HashMap<>();
158        this.tickLabelPaintMap = new HashMap<>();
159        this.categoryLabelToolTips = new HashMap<>();
160        this.categoryLabelURLs = new HashMap<>();
161    }
162
163    /**
164     * Returns the lower margin for the axis.
165     *
166     * @return The margin.
167     *
168     * @see #getUpperMargin()
169     * @see #setLowerMargin(double)
170     */
171    public double getLowerMargin() {
172        return this.lowerMargin;
173    }
174
175    /**
176     * Sets the lower margin for the axis and sends an {@link AxisChangeEvent}
177     * to all registered listeners.
178     *
179     * @param margin  the margin as a percentage of the axis length (for
180     *                example, 0.05 is five percent).
181     *
182     * @see #getLowerMargin()
183     */
184    public void setLowerMargin(double margin) {
185        this.lowerMargin = margin;
186        fireChangeEvent();
187    }
188
189    /**
190     * Returns the upper margin for the axis.
191     *
192     * @return The margin.
193     *
194     * @see #getLowerMargin()
195     * @see #setUpperMargin(double)
196     */
197    public double getUpperMargin() {
198        return this.upperMargin;
199    }
200
201    /**
202     * Sets the upper margin for the axis and sends an {@link AxisChangeEvent}
203     * to all registered listeners.
204     *
205     * @param margin  the margin as a percentage of the axis length (for
206     *                example, 0.05 is five percent).
207     *
208     * @see #getUpperMargin()
209     */
210    public void setUpperMargin(double margin) {
211        this.upperMargin = margin;
212        fireChangeEvent();
213    }
214
215    /**
216     * Returns the category margin.
217     *
218     * @return The margin.
219     *
220     * @see #setCategoryMargin(double)
221     */
222    public double getCategoryMargin() {
223        return this.categoryMargin;
224    }
225
226    /**
227     * Sets the category margin and sends an {@link AxisChangeEvent} to all
228     * registered listeners.  The overall category margin is distributed over
229     * N-1 gaps, where N is the number of categories on the axis.
230     *
231     * @param margin  the margin as a percentage of the axis length (for
232     *                example, 0.05 is five percent).
233     *
234     * @see #getCategoryMargin()
235     */
236    public void setCategoryMargin(double margin) {
237        this.categoryMargin = margin;
238        fireChangeEvent();
239    }
240
241    /**
242     * Returns the maximum number of lines to use for each category label.
243     *
244     * @return The maximum number of lines.
245     *
246     * @see #setMaximumCategoryLabelLines(int)
247     */
248    public int getMaximumCategoryLabelLines() {
249        return this.maximumCategoryLabelLines;
250    }
251
252    /**
253     * Sets the maximum number of lines to use for each category label and
254     * sends an {@link AxisChangeEvent} to all registered listeners.
255     *
256     * @param lines  the maximum number of lines.
257     *
258     * @see #getMaximumCategoryLabelLines()
259     */
260    public void setMaximumCategoryLabelLines(int lines) {
261        this.maximumCategoryLabelLines = lines;
262        fireChangeEvent();
263    }
264
265    /**
266     * Returns the category label width ratio.
267     *
268     * @return The ratio.
269     *
270     * @see #setMaximumCategoryLabelWidthRatio(float)
271     */
272    public float getMaximumCategoryLabelWidthRatio() {
273        return this.maximumCategoryLabelWidthRatio;
274    }
275
276    /**
277     * Sets the maximum category label width ratio and sends an
278     * {@link AxisChangeEvent} to all registered listeners.
279     *
280     * @param ratio  the ratio.
281     *
282     * @see #getMaximumCategoryLabelWidthRatio()
283     */
284    public void setMaximumCategoryLabelWidthRatio(float ratio) {
285        this.maximumCategoryLabelWidthRatio = ratio;
286        fireChangeEvent();
287    }
288
289    /**
290     * Returns the offset between the axis and the category labels (before
291     * label positioning is taken into account).
292     *
293     * @return The offset (in Java2D units).
294     *
295     * @see #setCategoryLabelPositionOffset(int)
296     */
297    public int getCategoryLabelPositionOffset() {
298        return this.categoryLabelPositionOffset;
299    }
300
301    /**
302     * Sets the offset between the axis and the category labels (before label
303     * positioning is taken into account) and sends a change event to all 
304     * registered listeners.
305     *
306     * @param offset  the offset (in Java2D units).
307     *
308     * @see #getCategoryLabelPositionOffset()
309     */
310    public void setCategoryLabelPositionOffset(int offset) {
311        this.categoryLabelPositionOffset = offset;
312        fireChangeEvent();
313    }
314
315    /**
316     * Returns the category label position specification (this contains label
317     * positioning info for all four possible axis locations).
318     *
319     * @return The positions (never {@code null}).
320     *
321     * @see #setCategoryLabelPositions(CategoryLabelPositions)
322     */
323    public CategoryLabelPositions getCategoryLabelPositions() {
324        return this.categoryLabelPositions;
325    }
326
327    /**
328     * Sets the category label position specification for the axis and sends an
329     * {@link AxisChangeEvent} to all registered listeners.
330     *
331     * @param positions  the positions ({@code null} not permitted).
332     *
333     * @see #getCategoryLabelPositions()
334     */
335    public void setCategoryLabelPositions(CategoryLabelPositions positions) {
336        Args.nullNotPermitted(positions, "positions");
337        this.categoryLabelPositions = positions;
338        fireChangeEvent();
339    }
340
341    /**
342     * Returns the font for the tick label for the given category.
343     *
344     * @param category  the category ({@code null} not permitted).
345     *
346     * @return The font (never {@code null}).
347     *
348     * @see #setTickLabelFont(Comparable, Font)
349     */
350    public Font getTickLabelFont(Comparable category) {
351        Args.nullNotPermitted(category, "category");
352        Font result = this.tickLabelFontMap.get(category);
353        // if there is no specific font, use the general one...
354        if (result == null) {
355            result = getTickLabelFont();
356        }
357        return result;
358    }
359
360    /**
361     * Sets the font for the tick label for the specified category and sends
362     * an {@link AxisChangeEvent} to all registered listeners.
363     *
364     * @param category  the category ({@code null} not permitted).
365     * @param font  the font ({@code null} permitted).
366     *
367     * @see #getTickLabelFont(Comparable)
368     */
369    public void setTickLabelFont(Comparable category, Font font) {
370        Args.nullNotPermitted(category, "category");
371        if (font == null) {
372            this.tickLabelFontMap.remove(category);
373        } else {
374            this.tickLabelFontMap.put(category, font);
375        }
376        fireChangeEvent();
377    }
378
379    /**
380     * Returns the paint for the tick label for the given category.
381     *
382     * @param category  the category ({@code null} not permitted).
383     *
384     * @return The paint (never {@code null}).
385     *
386     * @see #setTickLabelPaint(Paint)
387     */
388    public Paint getTickLabelPaint(Comparable category) {
389        Args.nullNotPermitted(category, "category");
390        Paint result = this.tickLabelPaintMap.get(category);
391        // if there is no specific paint, use the general one...
392        if (result == null) {
393            result = getTickLabelPaint();
394        }
395        return result;
396    }
397
398    /**
399     * Sets the paint for the tick label for the specified category and sends
400     * an {@link AxisChangeEvent} to all registered listeners.
401     *
402     * @param category  the category ({@code null} not permitted).
403     * @param paint  the paint ({@code null} permitted).
404     *
405     * @see #getTickLabelPaint(Comparable)
406     */
407    public void setTickLabelPaint(Comparable category, Paint paint) {
408        Args.nullNotPermitted(category, "category");
409        if (paint == null) {
410            this.tickLabelPaintMap.remove(category);
411        } else {
412            this.tickLabelPaintMap.put(category, paint);
413        }
414        fireChangeEvent();
415    }
416
417    /**
418     * Adds a tooltip to the specified category and sends an
419     * {@link AxisChangeEvent} to all registered listeners.
420     *
421     * @param category  the category ({@code null} not permitted).
422     * @param tooltip  the tooltip text ({@code null} permitted).
423     *
424     * @see #removeCategoryLabelToolTip(Comparable)
425     */
426    public void addCategoryLabelToolTip(Comparable category, String tooltip) {
427        Args.nullNotPermitted(category, "category");
428        this.categoryLabelToolTips.put(category, tooltip);
429        fireChangeEvent();
430    }
431
432    /**
433     * Returns the tool tip text for the label belonging to the specified
434     * category.
435     *
436     * @param category  the category ({@code null} not permitted).
437     *
438     * @return The tool tip text (possibly {@code null}).
439     *
440     * @see #addCategoryLabelToolTip(Comparable, String)
441     * @see #removeCategoryLabelToolTip(Comparable)
442     */
443    public String getCategoryLabelToolTip(Comparable category) {
444        Args.nullNotPermitted(category, "category");
445        return this.categoryLabelToolTips.get(category);
446    }
447
448    /**
449     * Removes the tooltip for the specified category and, if there was a value
450     * associated with that category, sends an {@link AxisChangeEvent} to all 
451     * registered listeners.
452     *
453     * @param category  the category ({@code null} not permitted).
454     *
455     * @see #addCategoryLabelToolTip(Comparable, String)
456     * @see #clearCategoryLabelToolTips()
457     */
458    public void removeCategoryLabelToolTip(Comparable category) {
459        Args.nullNotPermitted(category, "category");
460        if (this.categoryLabelToolTips.remove(category) != null) {
461            fireChangeEvent();
462        }
463    }
464
465    /**
466     * Clears the category label tooltips and sends an {@link AxisChangeEvent}
467     * to all registered listeners.
468     *
469     * @see #addCategoryLabelToolTip(Comparable, String)
470     * @see #removeCategoryLabelToolTip(Comparable)
471     */
472    public void clearCategoryLabelToolTips() {
473        this.categoryLabelToolTips.clear();
474        fireChangeEvent();
475    }
476
477    /**
478     * Adds a URL (to be used in image maps) to the specified category and 
479     * sends an {@link AxisChangeEvent} to all registered listeners.
480     *
481     * @param category  the category ({@code null} not permitted).
482     * @param url  the URL text ({@code null} permitted).
483     *
484     * @see #removeCategoryLabelURL(Comparable)
485     */
486    public void addCategoryLabelURL(Comparable category, String url) {
487        Args.nullNotPermitted(category, "category");
488        this.categoryLabelURLs.put(category, url);
489        fireChangeEvent();
490    }
491
492    /**
493     * Returns the URL for the label belonging to the specified category.
494     *
495     * @param category  the category ({@code null} not permitted).
496     *
497     * @return The URL text (possibly {@code null}).
498     * 
499     * @see #addCategoryLabelURL(Comparable, String)
500     * @see #removeCategoryLabelURL(Comparable)
501     */
502    public String getCategoryLabelURL(Comparable category) {
503        Args.nullNotPermitted(category, "category");
504        return this.categoryLabelURLs.get(category);
505    }
506
507    /**
508     * Removes the URL for the specified category and, if there was a URL 
509     * associated with that category, sends an {@link AxisChangeEvent} to all 
510     * registered listeners.
511     *
512     * @param category  the category ({@code null} not permitted).
513     *
514     * @see #addCategoryLabelURL(Comparable, String)
515     * @see #clearCategoryLabelURLs()
516     */
517    public void removeCategoryLabelURL(Comparable category) {
518        Args.nullNotPermitted(category, "category");
519        if (this.categoryLabelURLs.remove(category) != null) {
520            fireChangeEvent();
521        }
522    }
523
524    /**
525     * Clears the category label URLs and sends an {@link AxisChangeEvent}
526     * to all registered listeners.
527     *
528     * @see #addCategoryLabelURL(Comparable, String)
529     * @see #removeCategoryLabelURL(Comparable)
530     */
531    public void clearCategoryLabelURLs() {
532        this.categoryLabelURLs.clear();
533        fireChangeEvent();
534    }
535    
536    /**
537     * Returns the Java 2D coordinate for a category.
538     *
539     * @param anchor  the anchor point ({@code null} not permitted).
540     * @param category  the category index.
541     * @param categoryCount  the category count.
542     * @param area  the data area.
543     * @param edge  the location of the axis.
544     *
545     * @return The coordinate.
546     */
547    public double getCategoryJava2DCoordinate(CategoryAnchor anchor, 
548            int category, int categoryCount, Rectangle2D area, 
549            RectangleEdge edge) {
550        Args.nullNotPermitted(anchor, "anchor");
551        double result = 0.0;
552        switch (anchor) {
553            case START:
554                result = getCategoryStart(category, categoryCount, area, edge);
555                break;
556            case MIDDLE:
557                result = getCategoryMiddle(category, categoryCount, area, edge);
558                break;
559            case END:
560                result = getCategoryEnd(category, categoryCount, area, edge);
561                break;
562            default:
563                throw new IllegalStateException("Unexpected anchor value.");
564        }
565        return result;
566
567    }
568
569    /**
570     * Returns the starting coordinate for the specified category.
571     *
572     * @param category  the category.
573     * @param categoryCount  the number of categories.
574     * @param area  the data area.
575     * @param edge  the axis location.
576     *
577     * @return The coordinate.
578     *
579     * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
580     * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
581     */
582    public double getCategoryStart(int category, int categoryCount, 
583            Rectangle2D area, RectangleEdge edge) {
584
585        double result = 0.0;
586        if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
587            result = area.getX() + area.getWidth() * getLowerMargin();
588        }
589        else if ((edge == RectangleEdge.LEFT)
590                || (edge == RectangleEdge.RIGHT)) {
591            result = area.getMinY() + area.getHeight() * getLowerMargin();
592        }
593
594        double categorySize = calculateCategorySize(categoryCount, area, edge);
595        double categoryGapWidth = calculateCategoryGapSize(categoryCount, area,
596                edge);
597
598        result = result + category * (categorySize + categoryGapWidth);
599        return result;
600    }
601
602    /**
603     * Returns the middle coordinate for the specified category.
604     *
605     * @param category  the category.
606     * @param categoryCount  the number of categories.
607     * @param area  the data area.
608     * @param edge  the axis location.
609     *
610     * @return The coordinate.
611     *
612     * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
613     * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
614     */
615    public double getCategoryMiddle(int category, int categoryCount,
616            Rectangle2D area, RectangleEdge edge) {
617
618        if (category < 0 || category >= categoryCount) {
619            throw new IllegalArgumentException("Invalid category index: "
620                    + category);
621        }
622        return getCategoryStart(category, categoryCount, area, edge)
623               + calculateCategorySize(categoryCount, area, edge) / 2;
624
625    }
626
627    /**
628     * Returns the end coordinate for the specified category.
629     *
630     * @param category  the category.
631     * @param categoryCount  the number of categories.
632     * @param area  the data area.
633     * @param edge  the axis location.
634     *
635     * @return The coordinate.
636     *
637     * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
638     * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
639     */
640    public double getCategoryEnd(int category, int categoryCount,
641            Rectangle2D area, RectangleEdge edge) {
642        return getCategoryStart(category, categoryCount, area, edge)
643               + calculateCategorySize(categoryCount, area, edge);
644    }
645
646    /**
647     * A convenience method that returns the axis coordinate for the centre of
648     * a category.
649     *
650     * @param category  the category key ({@code null} not permitted).
651     * @param categories  the categories ({@code null} not permitted).
652     * @param area  the data area ({@code null} not permitted).
653     * @param edge  the edge along which the axis lies ({@code null} not
654     *     permitted).
655     *
656     * @return The centre coordinate.
657     *
658     * @see #getCategorySeriesMiddle(Comparable, Comparable, CategoryDataset,
659     *     double, Rectangle2D, RectangleEdge)
660     */
661    public double getCategoryMiddle(Comparable category,
662            List categories, Rectangle2D area, RectangleEdge edge) {
663        Args.nullNotPermitted(categories, "categories");
664        int categoryIndex = categories.indexOf(category);
665        int categoryCount = categories.size();
666        return getCategoryMiddle(categoryIndex, categoryCount, area, edge);
667    }
668
669    /**
670     * Returns the middle coordinate (in Java2D space) for a series within a
671     * category.
672     *
673     * @param category  the category ({@code null} not permitted).
674     * @param seriesKey  the series key ({@code null} not permitted).
675     * @param dataset  the dataset ({@code null} not permitted).
676     * @param itemMargin  the item margin (0.0 &lt;= itemMargin &lt; 1.0);
677     * @param area  the area ({@code null} not permitted).
678     * @param edge  the edge ({@code null} not permitted).
679     *
680     * @return The coordinate in Java2D space.
681     */
682    public double getCategorySeriesMiddle(Comparable category,
683            Comparable seriesKey, CategoryDataset dataset, double itemMargin,
684            Rectangle2D area, RectangleEdge edge) {
685
686        int categoryIndex = dataset.getColumnIndex(category);
687        int categoryCount = dataset.getColumnCount();
688        int seriesIndex = dataset.getRowIndex(seriesKey);
689        int seriesCount = dataset.getRowCount();
690        double start = getCategoryStart(categoryIndex, categoryCount, area,
691                edge);
692        double end = getCategoryEnd(categoryIndex, categoryCount, area, edge);
693        double width = end - start;
694        if (seriesCount == 1) {
695            return start + width / 2.0;
696        }
697        else {
698            double gap = (width * itemMargin) / (seriesCount - 1);
699            double ww = (width * (1 - itemMargin)) / seriesCount;
700            return start + (seriesIndex * (ww + gap)) + ww / 2.0;
701        }
702    }
703
704    /**
705     * Returns the middle coordinate (in Java2D space) for a series within a
706     * category.
707     *
708     * @param categoryIndex  the category index.
709     * @param categoryCount  the category count.
710     * @param seriesIndex the series index.
711     * @param seriesCount the series count.
712     * @param itemMargin  the item margin (0.0 &lt;= itemMargin &lt; 1.0);
713     * @param area  the area ({@code null} not permitted).
714     * @param edge  the edge ({@code null} not permitted).
715     *
716     * @return The coordinate in Java2D space.
717     */
718    public double getCategorySeriesMiddle(int categoryIndex, int categoryCount,
719            int seriesIndex, int seriesCount, double itemMargin,
720            Rectangle2D area, RectangleEdge edge) {
721
722        double start = getCategoryStart(categoryIndex, categoryCount, area,
723                edge);
724        double end = getCategoryEnd(categoryIndex, categoryCount, area, edge);
725        double width = end - start;
726        if (seriesCount == 1) {
727            return start + width / 2.0;
728        }
729        else {
730            double gap = (width * itemMargin) / (seriesCount - 1);
731            double ww = (width * (1 - itemMargin)) / seriesCount;
732            return start + (seriesIndex * (ww + gap)) + ww / 2.0;
733        }
734    }
735
736    /**
737     * Calculates the size (width or height, depending on the location of the
738     * axis) of a category.
739     *
740     * @param categoryCount  the number of categories.
741     * @param area  the area within which the categories will be drawn.
742     * @param edge  the axis location.
743     *
744     * @return The category size.
745     */
746    protected double calculateCategorySize(int categoryCount, Rectangle2D area,
747            RectangleEdge edge) {
748        double result;
749        double available = 0.0;
750
751        if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
752            available = area.getWidth();
753        }
754        else if ((edge == RectangleEdge.LEFT)
755                || (edge == RectangleEdge.RIGHT)) {
756            available = area.getHeight();
757        }
758        if (categoryCount > 1) {
759            result = available * (1 - getLowerMargin() - getUpperMargin()
760                     - getCategoryMargin());
761            result = result / categoryCount;
762        }
763        else {
764            result = available * (1 - getLowerMargin() - getUpperMargin());
765        }
766        return result;
767    }
768
769    /**
770     * Calculates the size (width or height, depending on the location of the
771     * axis) of a category gap.
772     *
773     * @param categoryCount  the number of categories.
774     * @param area  the area within which the categories will be drawn.
775     * @param edge  the axis location.
776     *
777     * @return The category gap width.
778     */
779    protected double calculateCategoryGapSize(int categoryCount, 
780            Rectangle2D area, RectangleEdge edge) {
781
782        double result = 0.0;
783        double available = 0.0;
784
785        if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
786            available = area.getWidth();
787        }
788        else if ((edge == RectangleEdge.LEFT)
789                || (edge == RectangleEdge.RIGHT)) {
790            available = area.getHeight();
791        }
792
793        if (categoryCount > 1) {
794            result = available * getCategoryMargin() / (categoryCount - 1);
795        }
796        return result;
797    }
798
799    /**
800     * Estimates the space required for the axis, given a specific drawing area.
801     *
802     * @param g2  the graphics device (used to obtain font information).
803     * @param plot  the plot that the axis belongs to.
804     * @param plotArea  the area within which the axis should be drawn.
805     * @param edge  the axis location ({@code null} not permitted).
806     * @param space  the space already reserved.
807     *
808     * @return The space required to draw the axis.
809     */
810    @Override
811    public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 
812            Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
813
814        // create a new space object if one wasn't supplied...
815        if (space == null) {
816            space = new AxisSpace();
817        }
818
819        // if the axis is not visible, no additional space is required...
820        if (!isVisible()) {
821            return space;
822        }
823
824        // calculate the max size of the tick labels (if visible)...
825        double tickLabelHeight = 0.0;
826        double tickLabelWidth = 0.0;
827        if (isTickLabelsVisible()) {
828            g2.setFont(getTickLabelFont());
829            AxisState state = new AxisState();
830            // we call refresh ticks just to get the maximum width or height
831            refreshTicks(g2, state, plotArea, edge);
832            switch (edge) {
833                case TOP:
834                    tickLabelHeight = state.getMax();
835                    break;
836                case BOTTOM:
837                    tickLabelHeight = state.getMax();
838                    break;
839                case LEFT:
840                    tickLabelWidth = state.getMax();
841                    break;
842                case RIGHT:
843                    tickLabelWidth = state.getMax();
844                    break;
845                default:
846                    throw new IllegalStateException("Unexpected RectangleEdge value.");
847            }
848        }
849
850        // get the axis label size and update the space object...
851        Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
852        double labelHeight, labelWidth;
853        if (RectangleEdge.isTopOrBottom(edge)) {
854            labelHeight = labelEnclosure.getHeight();
855            space.add(labelHeight + tickLabelHeight
856                    + this.categoryLabelPositionOffset, edge);
857        }
858        else if (RectangleEdge.isLeftOrRight(edge)) {
859            labelWidth = labelEnclosure.getWidth();
860            space.add(labelWidth + tickLabelWidth
861                    + this.categoryLabelPositionOffset, edge);
862        }
863        return space;
864    }
865
866    /**
867     * Configures the axis against the current plot.
868     */
869    @Override
870    public void configure() {
871        // nothing required
872    }
873
874    /**
875     * Draws the axis on a Java 2D graphics device (such as the screen or a
876     * printer).
877     *
878     * @param g2  the graphics device ({@code null} not permitted).
879     * @param cursor  the cursor location.
880     * @param plotArea  the area within which the axis should be drawn
881     *                  ({@code null} not permitted).
882     * @param dataArea  the area within which the plot is being drawn
883     *                  ({@code null} not permitted).
884     * @param edge  the location of the axis ({@code null} not permitted).
885     * @param plotState  collects information about the plot
886     *                   ({@code null} permitted).
887     *
888     * @return The axis state (never {@code null}).
889     */
890    @Override
891    public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
892            Rectangle2D dataArea, RectangleEdge edge,
893            PlotRenderingInfo plotState) {
894
895        // if the axis is not visible, don't draw it...
896        if (!isVisible()) {
897            return new AxisState(cursor);
898        }
899
900        if (isAxisLineVisible()) {
901            drawAxisLine(g2, cursor, dataArea, edge);
902        }
903        AxisState state = new AxisState(cursor);
904        if (isTickMarksVisible()) {
905            drawTickMarks(g2, cursor, dataArea, edge, state);
906        }
907
908        createAndAddEntity(cursor, state, dataArea, edge, plotState);
909
910        // draw the category labels and axis label
911        state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
912                plotState);
913        if (getAttributedLabel() != null) {
914            state = drawAttributedLabel(getAttributedLabel(), g2, plotArea, 
915                    dataArea, edge, state);
916            
917        } else {
918            state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
919        }
920        return state;
921
922    }
923
924    /**
925     * Draws the category labels and returns the updated axis state.
926     *
927     * @param g2  the graphics device ({@code null} not permitted).
928     * @param plotArea  the plot area ({@code null} not permitted).
929     * @param dataArea  the area inside the axes ({@code null} not
930     *                  permitted).
931     * @param edge  the axis location ({@code null} not permitted).
932     * @param state  the axis state ({@code null} not permitted).
933     * @param plotState  collects information about the plot ({@code null}
934     *                   permitted).
935     *
936     * @return The updated axis state (never {@code null}).
937     */
938    protected AxisState drawCategoryLabels(Graphics2D g2, Rectangle2D plotArea,
939            Rectangle2D dataArea, RectangleEdge edge, AxisState state,
940            PlotRenderingInfo plotState) {
941
942        Args.nullNotPermitted(state, "state");
943        if (!isTickLabelsVisible()) {
944            return state;
945        }
946 
947        List ticks = refreshTicks(g2, state, plotArea, edge);
948        state.setTicks(ticks);
949        int categoryIndex = 0;
950        for (Object o : ticks) {
951            CategoryTick tick = (CategoryTick) o;
952            g2.setFont(getTickLabelFont(tick.getCategory()));
953            g2.setPaint(getTickLabelPaint(tick.getCategory()));
954
955            CategoryLabelPosition position
956                    = this.categoryLabelPositions.getLabelPosition(edge);
957            double x0 = 0.0;
958            double x1 = 0.0;
959            double y0 = 0.0;
960            double y1 = 0.0;
961            if (edge == RectangleEdge.TOP) {
962                x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 
963                        edge);
964                x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 
965                        edge);
966                y1 = state.getCursor() - this.categoryLabelPositionOffset;
967                y0 = y1 - state.getMax();
968            }
969            else if (edge == RectangleEdge.BOTTOM) {
970                x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 
971                        edge);
972                x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 
973                        edge);
974                y0 = state.getCursor() + this.categoryLabelPositionOffset;
975                y1 = y0 + state.getMax();
976            }
977            else if (edge == RectangleEdge.LEFT) {
978                y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 
979                        edge);
980                y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
981                        edge);
982                x1 = state.getCursor() - this.categoryLabelPositionOffset;
983                x0 = x1 - state.getMax();
984            }
985            else if (edge == RectangleEdge.RIGHT) {
986                y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 
987                        edge);
988                y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea,
989                        edge);
990                x0 = state.getCursor() + this.categoryLabelPositionOffset;
991                x1 = x0 - state.getMax();
992            }
993            Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
994                    (y1 - y0));
995            Point2D anchorPoint = position.getCategoryAnchor().getAnchorPoint(area);
996            TextBlock block = tick.getLabel();
997            block.draw(g2, (float) anchorPoint.getX(),
998                    (float) anchorPoint.getY(), position.getLabelAnchor(),
999                    (float) anchorPoint.getX(), (float) anchorPoint.getY(),
1000                    position.getAngle());
1001            Shape bounds = block.calculateBounds(g2,
1002                    (float) anchorPoint.getX(), (float) anchorPoint.getY(),
1003                    position.getLabelAnchor(), (float) anchorPoint.getX(),
1004                    (float) anchorPoint.getY(), position.getAngle());
1005            if (plotState != null && plotState.getOwner() != null) {
1006                EntityCollection entities = plotState.getOwner()
1007                        .getEntityCollection();
1008                if (entities != null) {
1009                    String tooltip = getCategoryLabelToolTip(
1010                            tick.getCategory());
1011                    String url = getCategoryLabelURL(tick.getCategory());
1012                    entities.add(new CategoryLabelEntity(tick.getCategory(),
1013                            bounds, tooltip, url));
1014                }
1015            }
1016            categoryIndex++;
1017        }
1018
1019        if (edge.equals(RectangleEdge.TOP)) {
1020            double h = state.getMax() + this.categoryLabelPositionOffset;
1021            state.cursorUp(h);
1022        }
1023        else if (edge.equals(RectangleEdge.BOTTOM)) {
1024            double h = state.getMax() + this.categoryLabelPositionOffset;
1025            state.cursorDown(h);
1026        }
1027        else if (edge == RectangleEdge.LEFT) {
1028            double w = state.getMax() + this.categoryLabelPositionOffset;
1029            state.cursorLeft(w);
1030        }
1031        else if (edge == RectangleEdge.RIGHT) {
1032            double w = state.getMax() + this.categoryLabelPositionOffset;
1033            state.cursorRight(w);
1034        }
1035        return state;
1036    }
1037
1038    /**
1039     * Creates a temporary list of ticks that can be used when drawing the axis.
1040     *
1041     * @param g2  the graphics device (used to get font measurements).
1042     * @param state  the axis state.
1043     * @param dataArea  the area inside the axes.
1044     * @param edge  the location of the axis.
1045     *
1046     * @return A list of ticks.
1047     */
1048    @Override
1049    public List refreshTicks(Graphics2D g2, AxisState state, 
1050            Rectangle2D dataArea, RectangleEdge edge) {
1051
1052        List ticks = new java.util.ArrayList(); // FIXME generics
1053
1054        // sanity check for data area...
1055        if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
1056            return ticks;
1057        }
1058
1059        CategoryPlot plot = (CategoryPlot) getPlot();
1060        List categories = plot.getCategoriesForAxis(this);
1061        double max = 0.0;
1062
1063        if (categories != null) {
1064            CategoryLabelPosition position
1065                    = this.categoryLabelPositions.getLabelPosition(edge);
1066            float r = this.maximumCategoryLabelWidthRatio;
1067            if (r <= 0.0) {
1068                r = position.getWidthRatio();
1069            }
1070
1071            float l;
1072            if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {
1073                l = (float) calculateCategorySize(categories.size(), dataArea,
1074                        edge);
1075            }
1076            else {
1077                if (RectangleEdge.isLeftOrRight(edge)) {
1078                    l = (float) dataArea.getWidth();
1079                }
1080                else {
1081                    l = (float) dataArea.getHeight();
1082                }
1083            }
1084            int categoryIndex = 0;
1085            for (Object o : categories) {
1086                Comparable category = (Comparable) o;
1087                g2.setFont(getTickLabelFont(category));
1088                TextBlock label = createLabel(category, l * r, edge, g2);
1089                if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
1090                    max = Math.max(max, calculateCategoryLabelHeight(label,
1091                            position, getTickLabelInsets(), g2));
1092                } else if (edge == RectangleEdge.LEFT
1093                        || edge == RectangleEdge.RIGHT) {
1094                    max = Math.max(max, calculateCategoryLabelWidth(label,
1095                            position, getTickLabelInsets(), g2));
1096                }
1097                Tick tick = new CategoryTick(category, label,
1098                        position.getLabelAnchor(),
1099                        position.getRotationAnchor(), position.getAngle());
1100                ticks.add(tick);
1101                categoryIndex = categoryIndex + 1;
1102            }
1103        }
1104        state.setMax(max);
1105        return ticks;
1106
1107    }
1108
1109    /**
1110     * Draws the tick marks.
1111     * 
1112     * @param g2  the graphics target.
1113     * @param cursor  the cursor position (an offset when drawing multiple axes)
1114     * @param dataArea  the area for plotting the data.
1115     * @param edge  the location of the axis.
1116     * @param state  the axis state.
1117     */
1118    public void drawTickMarks(Graphics2D g2, double cursor,
1119            Rectangle2D dataArea, RectangleEdge edge, AxisState state) {
1120
1121        Plot p = getPlot();
1122        if (p == null) {
1123            return;
1124        }
1125        CategoryPlot plot = (CategoryPlot) p;
1126        double il = getTickMarkInsideLength();
1127        double ol = getTickMarkOutsideLength();
1128        Line2D line = new Line2D.Double();
1129        List<Comparable> categories = plot.getCategoriesForAxis(this);
1130        g2.setPaint(getTickMarkPaint());
1131        g2.setStroke(getTickMarkStroke());
1132        Object saved = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
1133        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
1134                RenderingHints.VALUE_STROKE_NORMALIZE);
1135        if (edge.equals(RectangleEdge.TOP)) {
1136            for (Comparable category : categories) {
1137                double x = getCategoryMiddle(category, categories, dataArea, edge);
1138                line.setLine(x, cursor, x, cursor + il);
1139                g2.draw(line);
1140                line.setLine(x, cursor, x, cursor - ol);
1141                g2.draw(line);
1142            }
1143            state.cursorUp(ol);
1144        } else if (edge.equals(RectangleEdge.BOTTOM)) {
1145            for (Comparable category : categories) {
1146                double x = getCategoryMiddle(category, categories, dataArea, edge);
1147                line.setLine(x, cursor, x, cursor - il);
1148                g2.draw(line);
1149                line.setLine(x, cursor, x, cursor + ol);
1150                g2.draw(line);
1151            }
1152            state.cursorDown(ol);
1153        } else if (edge.equals(RectangleEdge.LEFT)) {
1154            for (Comparable category : categories) {
1155                double y = getCategoryMiddle(category, categories, dataArea, edge);
1156                line.setLine(cursor, y, cursor + il, y);
1157                g2.draw(line);
1158                line.setLine(cursor, y, cursor - ol, y);
1159                g2.draw(line);
1160            }
1161            state.cursorLeft(ol);
1162        } else if (edge.equals(RectangleEdge.RIGHT)) {
1163            for (Comparable category : categories) {
1164                double y = getCategoryMiddle(category, categories, dataArea, edge);
1165                line.setLine(cursor, y, cursor - il, y);
1166                g2.draw(line);
1167                line.setLine(cursor, y, cursor + ol, y);
1168                g2.draw(line);
1169            }
1170            state.cursorRight(ol);
1171        }
1172        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, saved);
1173    }
1174
1175    /**
1176     * Creates a label.
1177     *
1178     * @param category  the category.
1179     * @param width  the available width.
1180     * @param edge  the edge on which the axis appears.
1181     * @param g2  the graphics device.
1182     *
1183     * @return A label.
1184     */
1185    protected TextBlock createLabel(Comparable category, float width,
1186            RectangleEdge edge, Graphics2D g2) {
1187        TextBlock label = TextUtils.createTextBlock(category.toString(),
1188                getTickLabelFont(category), getTickLabelPaint(category), width,
1189                this.maximumCategoryLabelLines, new G2TextMeasurer(g2));
1190        return label;
1191    }
1192
1193    /**
1194     * Calculates the width of a category label when rendered.
1195     *
1196     * @param label  the text block ({@code null} not permitted).
1197     * @param position  the position.
1198     * @param insets  the label insets.
1199     * @param g2  the graphics device.
1200     *
1201     * @return The width.
1202     */
1203    protected double calculateCategoryLabelWidth(TextBlock label, 
1204            CategoryLabelPosition position, RectangleInsets insets, Graphics2D g2) {
1205        Size2D size = label.calculateDimensions(g2);
1206        Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1207                size.getHeight());
1208        Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(),
1209                0.0f, 0.0f);
1210        double w = rotatedBox.getBounds2D().getWidth() + insets.getLeft()
1211                + insets.getRight();
1212        return w;
1213    }
1214
1215    /**
1216     * Calculates the height of a category label when rendered.
1217     *
1218     * @param block  the text block ({@code null} not permitted).
1219     * @param position  the label position ({@code null} not permitted).
1220     * @param insets  the label insets ({@code null} not permitted).
1221     * @param g2  the graphics device ({@code null} not permitted).
1222     *
1223     * @return The height.
1224     */
1225    protected double calculateCategoryLabelHeight(TextBlock block,
1226            CategoryLabelPosition position, RectangleInsets insets, Graphics2D g2) {
1227        Size2D size = block.calculateDimensions(g2);
1228        Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(),
1229                size.getHeight());
1230        Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(),
1231                0.0f, 0.0f);
1232        double h = rotatedBox.getBounds2D().getHeight()
1233                   + insets.getTop() + insets.getBottom();
1234        return h;
1235    }
1236
1237    /**
1238     * Creates a clone of the axis.
1239     *
1240     * @return A clone.
1241     *
1242     * @throws CloneNotSupportedException if some component of the axis does
1243     *         not support cloning.
1244     */
1245    @Override
1246    public Object clone() throws CloneNotSupportedException {
1247        CategoryAxis clone = (CategoryAxis) super.clone();
1248        clone.tickLabelFontMap = new HashMap<>(this.tickLabelFontMap);
1249        clone.tickLabelPaintMap = new HashMap<>(this.tickLabelPaintMap);
1250        clone.categoryLabelToolTips = new HashMap<>(this.categoryLabelToolTips);
1251        clone.categoryLabelURLs = new HashMap<>(this.categoryLabelToolTips);
1252        return clone;
1253    }
1254
1255    /**
1256     * Tests this axis for equality with an arbitrary object.
1257     *
1258     * @param obj  the object ({@code null} permitted).
1259     *
1260     * @return A boolean.
1261     */
1262    @Override
1263    public boolean equals(Object obj) {
1264        if (obj == this) {
1265            return true;
1266        }
1267        if (!(obj instanceof CategoryAxis)) {
1268            return false;
1269        }
1270        if (!super.equals(obj)) {
1271            return false;
1272        }
1273        CategoryAxis that = (CategoryAxis) obj;
1274        if (that.lowerMargin != this.lowerMargin) {
1275            return false;
1276        }
1277        if (that.upperMargin != this.upperMargin) {
1278            return false;
1279        }
1280        if (that.categoryMargin != this.categoryMargin) {
1281            return false;
1282        }
1283        if (that.maximumCategoryLabelWidthRatio
1284                != this.maximumCategoryLabelWidthRatio) {
1285            return false;
1286        }
1287        if (that.categoryLabelPositionOffset
1288                != this.categoryLabelPositionOffset) {
1289            return false;
1290        }
1291        if (!Objects.equals(that.categoryLabelPositions, this.categoryLabelPositions)) {
1292            return false;
1293        }
1294        if (!Objects.equals(that.categoryLabelToolTips, this.categoryLabelToolTips)) {
1295            return false;
1296        }
1297        if (!Objects.equals(this.categoryLabelURLs, that.categoryLabelURLs)) {
1298            return false;
1299        }
1300        if (!Objects.equals(this.tickLabelFontMap, that.tickLabelFontMap)) {
1301            return false;
1302        }
1303        if (!PaintUtils.equal(this.tickLabelPaintMap, that.tickLabelPaintMap)) {
1304            return false;
1305        }
1306        return true;
1307    }
1308
1309    /**
1310     * Returns a hash code for this object.
1311     *
1312     * @return A hash code.
1313     */
1314    @Override
1315    public int hashCode() {
1316        return super.hashCode();
1317    }
1318
1319    /**
1320     * Provides serialization support.
1321     *
1322     * @param stream  the output stream.
1323     *
1324     * @throws IOException  if there is an I/O error.
1325     */
1326    private void writeObject(ObjectOutputStream stream) throws IOException {
1327        stream.defaultWriteObject();
1328        writePaintMap(this.tickLabelPaintMap, stream);
1329    }
1330
1331    /**
1332     * Provides serialization support.
1333     *
1334     * @param stream  the input stream.
1335     *
1336     * @throws IOException  if there is an I/O error.
1337     * @throws ClassNotFoundException  if there is a classpath problem.
1338     */
1339    private void readObject(ObjectInputStream stream)
1340        throws IOException, ClassNotFoundException {
1341        stream.defaultReadObject();
1342        this.tickLabelPaintMap = readPaintMap(stream);
1343    }
1344
1345    /**
1346     * Reads a {@code Map} of ({@code Comparable}, {@code Paint})
1347     * elements from a stream.
1348     *
1349     * @param in  the input stream.
1350     *
1351     * @return The map.
1352     *
1353     * @throws IOException
1354     * @throws ClassNotFoundException
1355     *
1356     * @see #writePaintMap(Map, ObjectOutputStream)
1357     */
1358    private Map readPaintMap(ObjectInputStream in)
1359            throws IOException, ClassNotFoundException {
1360        boolean isNull = in.readBoolean();
1361        if (isNull) {
1362            return null;
1363        }
1364        Map result = new HashMap();
1365        int count = in.readInt();
1366        for (int i = 0; i < count; i++) {
1367            Comparable category = (Comparable) in.readObject();
1368            Paint paint = SerialUtils.readPaint(in);
1369            result.put(category, paint);
1370        }
1371        return result;
1372    }
1373
1374    /**
1375     * Writes a map of ({@code Comparable}, {@code Paint})
1376     * elements to a stream.
1377     *
1378     * @param map  the map ({@code null} permitted).
1379     *
1380     * @param out
1381     * @throws IOException
1382     *
1383     * @see #readPaintMap(ObjectInputStream)
1384     */
1385    private void writePaintMap(Map map, ObjectOutputStream out)
1386            throws IOException {
1387        if (map == null) {
1388            out.writeBoolean(true);
1389        }
1390        else {
1391            out.writeBoolean(false);
1392            Set keys = map.keySet();
1393            int count = keys.size();
1394            out.writeInt(count);
1395            for (Object o : keys) {
1396                Comparable key = (Comparable) o;
1397                out.writeObject(key);
1398                SerialUtils.writePaint((Paint) map.get(key), out);
1399            }
1400        }
1401    }
1402
1403}