0001: /* ===========================================================
0002: * JFreeChart : a free chart library for the Java(tm) platform
0003: * ===========================================================
0004: *
0005: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
0006: *
0007: * Project Info: http://www.jfree.org/jfreechart/index.html
0008: *
0009: * This library is free software; you can redistribute it and/or modify it
0010: * under the terms of the GNU Lesser General Public License as published by
0011: * the Free Software Foundation; either version 2.1 of the License, or
0012: * (at your option) any later version.
0013: *
0014: * This library is distributed in the hope that it will be useful, but
0015: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
0016: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
0017: * License for more details.
0018: *
0019: * You should have received a copy of the GNU Lesser General Public
0020: * License along with this library; if not, write to the Free Software
0021: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
0022: * USA.
0023: *
0024: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
0025: * in the United States and other countries.]
0026: *
0027: * -----------------
0028: * CategoryAxis.java
0029: * -----------------
0030: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
0031: *
0032: * Original Author: David Gilbert;
0033: * Contributor(s): Pady Srinivasan (patch 1217634);
0034: *
0035: * $Id: CategoryAxis.java,v 1.18.2.12 2007/03/07 11:14:11 mungady Exp $
0036: *
0037: * Changes (from 21-Aug-2001)
0038: * --------------------------
0039: * 21-Aug-2001 : Added standard header. Fixed DOS encoding problem (DG);
0040: * 18-Sep-2001 : Updated header (DG);
0041: * 04-Dec-2001 : Changed constructors to protected, and tidied up default
0042: * values (DG);
0043: * 19-Apr-2002 : Updated import statements (DG);
0044: * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);
0045: * 06-Nov-2002 : Moved margins from the CategoryPlot class (DG);
0046: * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
0047: * 22-Jan-2002 : Removed monolithic constructor (DG);
0048: * 26-Mar-2003 : Implemented Serializable (DG);
0049: * 09-May-2003 : Merged HorizontalCategoryAxis and VerticalCategoryAxis into
0050: * this class (DG);
0051: * 13-Aug-2003 : Implemented Cloneable (DG);
0052: * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
0053: * 05-Nov-2003 : Fixed serialization bug (DG);
0054: * 26-Nov-2003 : Added category label offset (DG);
0055: * 06-Jan-2004 : Moved axis line attributes to Axis class, rationalised
0056: * category label position attributes (DG);
0057: * 07-Jan-2004 : Added new implementation for linewrapping of category
0058: * labels (DG);
0059: * 17-Feb-2004 : Moved deprecated code to bottom of source file (DG);
0060: * 10-Mar-2004 : Changed Dimension --> Dimension2D in text classes (DG);
0061: * 16-Mar-2004 : Added support for tooltips on category labels (DG);
0062: * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D
0063: * because of JDK bug 4976448 which persists on JDK 1.3.1 (DG);
0064: * 03-Sep-2004 : Added 'maxCategoryLabelLines' attribute (DG);
0065: * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG);
0066: * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
0067: * release (DG);
0068: * 21-Jan-2005 : Modified return type for RectangleAnchor.coordinates()
0069: * method (DG);
0070: * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG);
0071: * 26-Apr-2005 : Removed LOGGER (DG);
0072: * 08-Jun-2005 : Fixed bug in axis layout (DG);
0073: * 22-Nov-2005 : Added a method to access the tool tip text for a category
0074: * label (DG);
0075: * 23-Nov-2005 : Added per-category font and paint options - see patch
0076: * 1217634 (DG);
0077: * ------------- JFreeChart 1.0.x ---------------------------------------------
0078: * 11-Jan-2006 : Fixed null pointer exception in drawCategoryLabels - see bug
0079: * 1403043 (DG);
0080: * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
0081: * Joubert (1277726) (DG);
0082: * 02-Oct-2006 : Updated category label entity (DG);
0083: * 30-Oct-2006 : Updated refreshTicks() method to account for possibility of
0084: * multiple domain axes (DG);
0085: * 07-Mar-2007 : Fixed bug in axis label positioning (DG);
0086: *
0087: */
0088:
0089: package org.jfree.chart.axis;
0090:
0091: import java.awt.Font;
0092: import java.awt.Graphics2D;
0093: import java.awt.Paint;
0094: import java.awt.Shape;
0095: import java.awt.geom.Point2D;
0096: import java.awt.geom.Rectangle2D;
0097: import java.io.IOException;
0098: import java.io.ObjectInputStream;
0099: import java.io.ObjectOutputStream;
0100: import java.io.Serializable;
0101: import java.util.HashMap;
0102: import java.util.Iterator;
0103: import java.util.List;
0104: import java.util.Map;
0105: import java.util.Set;
0106:
0107: import org.jfree.chart.entity.CategoryLabelEntity;
0108: import org.jfree.chart.entity.EntityCollection;
0109: import org.jfree.chart.event.AxisChangeEvent;
0110: import org.jfree.chart.plot.CategoryPlot;
0111: import org.jfree.chart.plot.Plot;
0112: import org.jfree.chart.plot.PlotRenderingInfo;
0113: import org.jfree.io.SerialUtilities;
0114: import org.jfree.text.G2TextMeasurer;
0115: import org.jfree.text.TextBlock;
0116: import org.jfree.text.TextUtilities;
0117: import org.jfree.ui.RectangleAnchor;
0118: import org.jfree.ui.RectangleEdge;
0119: import org.jfree.ui.RectangleInsets;
0120: import org.jfree.ui.Size2D;
0121: import org.jfree.util.ObjectUtilities;
0122: import org.jfree.util.PaintUtilities;
0123: import org.jfree.util.ShapeUtilities;
0124:
0125: /**
0126: * An axis that displays categories.
0127: */
0128: public class CategoryAxis extends Axis implements Cloneable,
0129: Serializable {
0130:
0131: /** For serialization. */
0132: private static final long serialVersionUID = 5886554608114265863L;
0133:
0134: /**
0135: * The default margin for the axis (used for both lower and upper margins).
0136: */
0137: public static final double DEFAULT_AXIS_MARGIN = 0.05;
0138:
0139: /**
0140: * The default margin between categories (a percentage of the overall axis
0141: * length).
0142: */
0143: public static final double DEFAULT_CATEGORY_MARGIN = 0.20;
0144:
0145: /** The amount of space reserved at the start of the axis. */
0146: private double lowerMargin;
0147:
0148: /** The amount of space reserved at the end of the axis. */
0149: private double upperMargin;
0150:
0151: /** The amount of space reserved between categories. */
0152: private double categoryMargin;
0153:
0154: /** The maximum number of lines for category labels. */
0155: private int maximumCategoryLabelLines;
0156:
0157: /**
0158: * A ratio that is multiplied by the width of one category to determine the
0159: * maximum label width.
0160: */
0161: private float maximumCategoryLabelWidthRatio;
0162:
0163: /** The category label offset. */
0164: private int categoryLabelPositionOffset;
0165:
0166: /**
0167: * A structure defining the category label positions for each axis
0168: * location.
0169: */
0170: private CategoryLabelPositions categoryLabelPositions;
0171:
0172: /** Storage for tick label font overrides (if any). */
0173: private Map tickLabelFontMap;
0174:
0175: /** Storage for tick label paint overrides (if any). */
0176: private transient Map tickLabelPaintMap;
0177:
0178: /** Storage for the category label tooltips (if any). */
0179: private Map categoryLabelToolTips;
0180:
0181: /**
0182: * Creates a new category axis with no label.
0183: */
0184: public CategoryAxis() {
0185: this (null);
0186: }
0187:
0188: /**
0189: * Constructs a category axis, using default values where necessary.
0190: *
0191: * @param label the axis label (<code>null</code> permitted).
0192: */
0193: public CategoryAxis(String label) {
0194:
0195: super (label);
0196:
0197: this .lowerMargin = DEFAULT_AXIS_MARGIN;
0198: this .upperMargin = DEFAULT_AXIS_MARGIN;
0199: this .categoryMargin = DEFAULT_CATEGORY_MARGIN;
0200: this .maximumCategoryLabelLines = 1;
0201: this .maximumCategoryLabelWidthRatio = 0.0f;
0202:
0203: setTickMarksVisible(false); // not supported by this axis type yet
0204:
0205: this .categoryLabelPositionOffset = 4;
0206: this .categoryLabelPositions = CategoryLabelPositions.STANDARD;
0207: this .tickLabelFontMap = new HashMap();
0208: this .tickLabelPaintMap = new HashMap();
0209: this .categoryLabelToolTips = new HashMap();
0210:
0211: }
0212:
0213: /**
0214: * Returns the lower margin for the axis.
0215: *
0216: * @return The margin.
0217: *
0218: * @see #getUpperMargin()
0219: * @see #setLowerMargin(double)
0220: */
0221: public double getLowerMargin() {
0222: return this .lowerMargin;
0223: }
0224:
0225: /**
0226: * Sets the lower margin for the axis and sends an {@link AxisChangeEvent}
0227: * to all registered listeners.
0228: *
0229: * @param margin the margin as a percentage of the axis length (for
0230: * example, 0.05 is five percent).
0231: *
0232: * @see #getLowerMargin()
0233: */
0234: public void setLowerMargin(double margin) {
0235: this .lowerMargin = margin;
0236: notifyListeners(new AxisChangeEvent(this ));
0237: }
0238:
0239: /**
0240: * Returns the upper margin for the axis.
0241: *
0242: * @return The margin.
0243: *
0244: * @see #getLowerMargin()
0245: * @see #setUpperMargin(double)
0246: */
0247: public double getUpperMargin() {
0248: return this .upperMargin;
0249: }
0250:
0251: /**
0252: * Sets the upper margin for the axis and sends an {@link AxisChangeEvent}
0253: * to all registered listeners.
0254: *
0255: * @param margin the margin as a percentage of the axis length (for
0256: * example, 0.05 is five percent).
0257: *
0258: * @see #getUpperMargin()
0259: */
0260: public void setUpperMargin(double margin) {
0261: this .upperMargin = margin;
0262: notifyListeners(new AxisChangeEvent(this ));
0263: }
0264:
0265: /**
0266: * Returns the category margin.
0267: *
0268: * @return The margin.
0269: *
0270: * @see #setCategoryMargin(double)
0271: */
0272: public double getCategoryMargin() {
0273: return this .categoryMargin;
0274: }
0275:
0276: /**
0277: * Sets the category margin and sends an {@link AxisChangeEvent} to all
0278: * registered listeners. The overall category margin is distributed over
0279: * N-1 gaps, where N is the number of categories on the axis.
0280: *
0281: * @param margin the margin as a percentage of the axis length (for
0282: * example, 0.05 is five percent).
0283: *
0284: * @see #getCategoryMargin()
0285: */
0286: public void setCategoryMargin(double margin) {
0287: this .categoryMargin = margin;
0288: notifyListeners(new AxisChangeEvent(this ));
0289: }
0290:
0291: /**
0292: * Returns the maximum number of lines to use for each category label.
0293: *
0294: * @return The maximum number of lines.
0295: *
0296: * @see #setMaximumCategoryLabelLines(int)
0297: */
0298: public int getMaximumCategoryLabelLines() {
0299: return this .maximumCategoryLabelLines;
0300: }
0301:
0302: /**
0303: * Sets the maximum number of lines to use for each category label and
0304: * sends an {@link AxisChangeEvent} to all registered listeners.
0305: *
0306: * @param lines the maximum number of lines.
0307: *
0308: * @see #getMaximumCategoryLabelLines()
0309: */
0310: public void setMaximumCategoryLabelLines(int lines) {
0311: this .maximumCategoryLabelLines = lines;
0312: notifyListeners(new AxisChangeEvent(this ));
0313: }
0314:
0315: /**
0316: * Returns the category label width ratio.
0317: *
0318: * @return The ratio.
0319: *
0320: * @see #setMaximumCategoryLabelWidthRatio(float)
0321: */
0322: public float getMaximumCategoryLabelWidthRatio() {
0323: return this .maximumCategoryLabelWidthRatio;
0324: }
0325:
0326: /**
0327: * Sets the maximum category label width ratio and sends an
0328: * {@link AxisChangeEvent} to all registered listeners.
0329: *
0330: * @param ratio the ratio.
0331: *
0332: * @see #getMaximumCategoryLabelWidthRatio()
0333: */
0334: public void setMaximumCategoryLabelWidthRatio(float ratio) {
0335: this .maximumCategoryLabelWidthRatio = ratio;
0336: notifyListeners(new AxisChangeEvent(this ));
0337: }
0338:
0339: /**
0340: * Returns the offset between the axis and the category labels (before
0341: * label positioning is taken into account).
0342: *
0343: * @return The offset (in Java2D units).
0344: *
0345: * @see #setCategoryLabelPositionOffset(int)
0346: */
0347: public int getCategoryLabelPositionOffset() {
0348: return this .categoryLabelPositionOffset;
0349: }
0350:
0351: /**
0352: * Sets the offset between the axis and the category labels (before label
0353: * positioning is taken into account).
0354: *
0355: * @param offset the offset (in Java2D units).
0356: *
0357: * @see #getCategoryLabelPositionOffset()
0358: */
0359: public void setCategoryLabelPositionOffset(int offset) {
0360: this .categoryLabelPositionOffset = offset;
0361: notifyListeners(new AxisChangeEvent(this ));
0362: }
0363:
0364: /**
0365: * Returns the category label position specification (this contains label
0366: * positioning info for all four possible axis locations).
0367: *
0368: * @return The positions (never <code>null</code>).
0369: *
0370: * @see #setCategoryLabelPositions(CategoryLabelPositions)
0371: */
0372: public CategoryLabelPositions getCategoryLabelPositions() {
0373: return this .categoryLabelPositions;
0374: }
0375:
0376: /**
0377: * Sets the category label position specification for the axis and sends an
0378: * {@link AxisChangeEvent} to all registered listeners.
0379: *
0380: * @param positions the positions (<code>null</code> not permitted).
0381: *
0382: * @see #getCategoryLabelPositions()
0383: */
0384: public void setCategoryLabelPositions(
0385: CategoryLabelPositions positions) {
0386: if (positions == null) {
0387: throw new IllegalArgumentException(
0388: "Null 'positions' argument.");
0389: }
0390: this .categoryLabelPositions = positions;
0391: notifyListeners(new AxisChangeEvent(this ));
0392: }
0393:
0394: /**
0395: * Returns the font for the tick label for the given category.
0396: *
0397: * @param category the category (<code>null</code> not permitted).
0398: *
0399: * @return The font (never <code>null</code>).
0400: *
0401: * @see #setTickLabelFont(Comparable, Font)
0402: */
0403: public Font getTickLabelFont(Comparable category) {
0404: if (category == null) {
0405: throw new IllegalArgumentException(
0406: "Null 'category' argument.");
0407: }
0408: Font result = (Font) this .tickLabelFontMap.get(category);
0409: // if there is no specific font, use the general one...
0410: if (result == null) {
0411: result = getTickLabelFont();
0412: }
0413: return result;
0414: }
0415:
0416: /**
0417: * Sets the font for the tick label for the specified category and sends
0418: * an {@link AxisChangeEvent} to all registered listeners.
0419: *
0420: * @param category the category (<code>null</code> not permitted).
0421: * @param font the font (<code>null</code> permitted).
0422: *
0423: * @see #getTickLabelFont(Comparable)
0424: */
0425: public void setTickLabelFont(Comparable category, Font font) {
0426: if (category == null) {
0427: throw new IllegalArgumentException(
0428: "Null 'category' argument.");
0429: }
0430: if (font == null) {
0431: this .tickLabelFontMap.remove(category);
0432: } else {
0433: this .tickLabelFontMap.put(category, font);
0434: }
0435: notifyListeners(new AxisChangeEvent(this ));
0436: }
0437:
0438: /**
0439: * Returns the paint for the tick label for the given category.
0440: *
0441: * @param category the category (<code>null</code> not permitted).
0442: *
0443: * @return The paint (never <code>null</code>).
0444: *
0445: * @see #setTickLabelPaint(Paint)
0446: */
0447: public Paint getTickLabelPaint(Comparable category) {
0448: if (category == null) {
0449: throw new IllegalArgumentException(
0450: "Null 'category' argument.");
0451: }
0452: Paint result = (Paint) this .tickLabelPaintMap.get(category);
0453: // if there is no specific paint, use the general one...
0454: if (result == null) {
0455: result = getTickLabelPaint();
0456: }
0457: return result;
0458: }
0459:
0460: /**
0461: * Sets the paint for the tick label for the specified category and sends
0462: * an {@link AxisChangeEvent} to all registered listeners.
0463: *
0464: * @param category the category (<code>null</code> not permitted).
0465: * @param paint the paint (<code>null</code> permitted).
0466: *
0467: * @see #getTickLabelPaint(Comparable)
0468: */
0469: public void setTickLabelPaint(Comparable category, Paint paint) {
0470: if (category == null) {
0471: throw new IllegalArgumentException(
0472: "Null 'category' argument.");
0473: }
0474: if (paint == null) {
0475: this .tickLabelPaintMap.remove(category);
0476: } else {
0477: this .tickLabelPaintMap.put(category, paint);
0478: }
0479: notifyListeners(new AxisChangeEvent(this ));
0480: }
0481:
0482: /**
0483: * Adds a tooltip to the specified category and sends an
0484: * {@link AxisChangeEvent} to all registered listeners.
0485: *
0486: * @param category the category (<code>null<code> not permitted).
0487: * @param tooltip the tooltip text (<code>null</code> permitted).
0488: *
0489: * @see #removeCategoryLabelToolTip(Comparable)
0490: */
0491: public void addCategoryLabelToolTip(Comparable category,
0492: String tooltip) {
0493: if (category == null) {
0494: throw new IllegalArgumentException(
0495: "Null 'category' argument.");
0496: }
0497: this .categoryLabelToolTips.put(category, tooltip);
0498: notifyListeners(new AxisChangeEvent(this ));
0499: }
0500:
0501: /**
0502: * Returns the tool tip text for the label belonging to the specified
0503: * category.
0504: *
0505: * @param category the category (<code>null</code> not permitted).
0506: *
0507: * @return The tool tip text (possibly <code>null</code>).
0508: *
0509: * @see #addCategoryLabelToolTip(Comparable, String)
0510: * @see #removeCategoryLabelToolTip(Comparable)
0511: */
0512: public String getCategoryLabelToolTip(Comparable category) {
0513: if (category == null) {
0514: throw new IllegalArgumentException(
0515: "Null 'category' argument.");
0516: }
0517: return (String) this .categoryLabelToolTips.get(category);
0518: }
0519:
0520: /**
0521: * Removes the tooltip for the specified category and sends an
0522: * {@link AxisChangeEvent} to all registered listeners.
0523: *
0524: * @param category the category (<code>null<code> not permitted).
0525: *
0526: * @see #addCategoryLabelToolTip(Comparable, String)
0527: * @see #clearCategoryLabelToolTips()
0528: */
0529: public void removeCategoryLabelToolTip(Comparable category) {
0530: if (category == null) {
0531: throw new IllegalArgumentException(
0532: "Null 'category' argument.");
0533: }
0534: this .categoryLabelToolTips.remove(category);
0535: notifyListeners(new AxisChangeEvent(this ));
0536: }
0537:
0538: /**
0539: * Clears the category label tooltips and sends an {@link AxisChangeEvent}
0540: * to all registered listeners.
0541: *
0542: * @see #addCategoryLabelToolTip(Comparable, String)
0543: * @see #removeCategoryLabelToolTip(Comparable)
0544: */
0545: public void clearCategoryLabelToolTips() {
0546: this .categoryLabelToolTips.clear();
0547: notifyListeners(new AxisChangeEvent(this ));
0548: }
0549:
0550: /**
0551: * Returns the Java 2D coordinate for a category.
0552: *
0553: * @param anchor the anchor point.
0554: * @param category the category index.
0555: * @param categoryCount the category count.
0556: * @param area the data area.
0557: * @param edge the location of the axis.
0558: *
0559: * @return The coordinate.
0560: */
0561: public double getCategoryJava2DCoordinate(CategoryAnchor anchor,
0562: int category, int categoryCount, Rectangle2D area,
0563: RectangleEdge edge) {
0564:
0565: double result = 0.0;
0566: if (anchor == CategoryAnchor.START) {
0567: result = getCategoryStart(category, categoryCount, area,
0568: edge);
0569: } else if (anchor == CategoryAnchor.MIDDLE) {
0570: result = getCategoryMiddle(category, categoryCount, area,
0571: edge);
0572: } else if (anchor == CategoryAnchor.END) {
0573: result = getCategoryEnd(category, categoryCount, area, edge);
0574: }
0575: return result;
0576:
0577: }
0578:
0579: /**
0580: * Returns the starting coordinate for the specified category.
0581: *
0582: * @param category the category.
0583: * @param categoryCount the number of categories.
0584: * @param area the data area.
0585: * @param edge the axis location.
0586: *
0587: * @return The coordinate.
0588: *
0589: * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
0590: * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
0591: */
0592: public double getCategoryStart(int category, int categoryCount,
0593: Rectangle2D area, RectangleEdge edge) {
0594:
0595: double result = 0.0;
0596: if ((edge == RectangleEdge.TOP)
0597: || (edge == RectangleEdge.BOTTOM)) {
0598: result = area.getX() + area.getWidth() * getLowerMargin();
0599: } else if ((edge == RectangleEdge.LEFT)
0600: || (edge == RectangleEdge.RIGHT)) {
0601: result = area.getMinY() + area.getHeight()
0602: * getLowerMargin();
0603: }
0604:
0605: double categorySize = calculateCategorySize(categoryCount,
0606: area, edge);
0607: double categoryGapWidth = calculateCategoryGapSize(
0608: categoryCount, area, edge);
0609:
0610: result = result + category * (categorySize + categoryGapWidth);
0611: return result;
0612:
0613: }
0614:
0615: /**
0616: * Returns the middle coordinate for the specified category.
0617: *
0618: * @param category the category.
0619: * @param categoryCount the number of categories.
0620: * @param area the data area.
0621: * @param edge the axis location.
0622: *
0623: * @return The coordinate.
0624: *
0625: * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
0626: * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge)
0627: */
0628: public double getCategoryMiddle(int category, int categoryCount,
0629: Rectangle2D area, RectangleEdge edge) {
0630:
0631: return getCategoryStart(category, categoryCount, area, edge)
0632: + calculateCategorySize(categoryCount, area, edge) / 2;
0633:
0634: }
0635:
0636: /**
0637: * Returns the end coordinate for the specified category.
0638: *
0639: * @param category the category.
0640: * @param categoryCount the number of categories.
0641: * @param area the data area.
0642: * @param edge the axis location.
0643: *
0644: * @return The coordinate.
0645: *
0646: * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge)
0647: * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge)
0648: */
0649: public double getCategoryEnd(int category, int categoryCount,
0650: Rectangle2D area, RectangleEdge edge) {
0651:
0652: return getCategoryStart(category, categoryCount, area, edge)
0653: + calculateCategorySize(categoryCount, area, edge);
0654:
0655: }
0656:
0657: /**
0658: * Calculates the size (width or height, depending on the location of the
0659: * axis) of a category.
0660: *
0661: * @param categoryCount the number of categories.
0662: * @param area the area within which the categories will be drawn.
0663: * @param edge the axis location.
0664: *
0665: * @return The category size.
0666: */
0667: protected double calculateCategorySize(int categoryCount,
0668: Rectangle2D area, RectangleEdge edge) {
0669:
0670: double result = 0.0;
0671: double available = 0.0;
0672:
0673: if ((edge == RectangleEdge.TOP)
0674: || (edge == RectangleEdge.BOTTOM)) {
0675: available = area.getWidth();
0676: } else if ((edge == RectangleEdge.LEFT)
0677: || (edge == RectangleEdge.RIGHT)) {
0678: available = area.getHeight();
0679: }
0680: if (categoryCount > 1) {
0681: result = available
0682: * (1 - getLowerMargin() - getUpperMargin() - getCategoryMargin());
0683: result = result / categoryCount;
0684: } else {
0685: result = available
0686: * (1 - getLowerMargin() - getUpperMargin());
0687: }
0688: return result;
0689:
0690: }
0691:
0692: /**
0693: * Calculates the size (width or height, depending on the location of the
0694: * axis) of a category gap.
0695: *
0696: * @param categoryCount the number of categories.
0697: * @param area the area within which the categories will be drawn.
0698: * @param edge the axis location.
0699: *
0700: * @return The category gap width.
0701: */
0702: protected double calculateCategoryGapSize(int categoryCount,
0703: Rectangle2D area, RectangleEdge edge) {
0704:
0705: double result = 0.0;
0706: double available = 0.0;
0707:
0708: if ((edge == RectangleEdge.TOP)
0709: || (edge == RectangleEdge.BOTTOM)) {
0710: available = area.getWidth();
0711: } else if ((edge == RectangleEdge.LEFT)
0712: || (edge == RectangleEdge.RIGHT)) {
0713: available = area.getHeight();
0714: }
0715:
0716: if (categoryCount > 1) {
0717: result = available * getCategoryMargin()
0718: / (categoryCount - 1);
0719: }
0720:
0721: return result;
0722:
0723: }
0724:
0725: /**
0726: * Estimates the space required for the axis, given a specific drawing area.
0727: *
0728: * @param g2 the graphics device (used to obtain font information).
0729: * @param plot the plot that the axis belongs to.
0730: * @param plotArea the area within which the axis should be drawn.
0731: * @param edge the axis location (top or bottom).
0732: * @param space the space already reserved.
0733: *
0734: * @return The space required to draw the axis.
0735: */
0736: public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
0737: Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) {
0738:
0739: // create a new space object if one wasn't supplied...
0740: if (space == null) {
0741: space = new AxisSpace();
0742: }
0743:
0744: // if the axis is not visible, no additional space is required...
0745: if (!isVisible()) {
0746: return space;
0747: }
0748:
0749: // calculate the max size of the tick labels (if visible)...
0750: double tickLabelHeight = 0.0;
0751: double tickLabelWidth = 0.0;
0752: if (isTickLabelsVisible()) {
0753: g2.setFont(getTickLabelFont());
0754: AxisState state = new AxisState();
0755: // we call refresh ticks just to get the maximum width or height
0756: refreshTicks(g2, state, plotArea, edge);
0757: if (edge == RectangleEdge.TOP) {
0758: tickLabelHeight = state.getMax();
0759: } else if (edge == RectangleEdge.BOTTOM) {
0760: tickLabelHeight = state.getMax();
0761: } else if (edge == RectangleEdge.LEFT) {
0762: tickLabelWidth = state.getMax();
0763: } else if (edge == RectangleEdge.RIGHT) {
0764: tickLabelWidth = state.getMax();
0765: }
0766: }
0767:
0768: // get the axis label size and update the space object...
0769: Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
0770: double labelHeight = 0.0;
0771: double labelWidth = 0.0;
0772: if (RectangleEdge.isTopOrBottom(edge)) {
0773: labelHeight = labelEnclosure.getHeight();
0774: space.add(labelHeight + tickLabelHeight
0775: + this .categoryLabelPositionOffset, edge);
0776: } else if (RectangleEdge.isLeftOrRight(edge)) {
0777: labelWidth = labelEnclosure.getWidth();
0778: space.add(labelWidth + tickLabelWidth
0779: + this .categoryLabelPositionOffset, edge);
0780: }
0781: return space;
0782:
0783: }
0784:
0785: /**
0786: * Configures the axis against the current plot.
0787: */
0788: public void configure() {
0789: // nothing required
0790: }
0791:
0792: /**
0793: * Draws the axis on a Java 2D graphics device (such as the screen or a
0794: * printer).
0795: *
0796: * @param g2 the graphics device (<code>null</code> not permitted).
0797: * @param cursor the cursor location.
0798: * @param plotArea the area within which the axis should be drawn
0799: * (<code>null</code> not permitted).
0800: * @param dataArea the area within which the plot is being drawn
0801: * (<code>null</code> not permitted).
0802: * @param edge the location of the axis (<code>null</code> not permitted).
0803: * @param plotState collects information about the plot
0804: * (<code>null</code> permitted).
0805: *
0806: * @return The axis state (never <code>null</code>).
0807: */
0808: public AxisState draw(Graphics2D g2, double cursor,
0809: Rectangle2D plotArea, Rectangle2D dataArea,
0810: RectangleEdge edge, PlotRenderingInfo plotState) {
0811:
0812: // if the axis is not visible, don't draw it...
0813: if (!isVisible()) {
0814: return new AxisState(cursor);
0815: }
0816:
0817: if (isAxisLineVisible()) {
0818: drawAxisLine(g2, cursor, dataArea, edge);
0819: }
0820:
0821: // draw the category labels and axis label
0822: AxisState state = new AxisState(cursor);
0823: state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
0824: plotState);
0825: state = drawLabel(getLabel(), g2, plotArea, dataArea, edge,
0826: state);
0827:
0828: return state;
0829:
0830: }
0831:
0832: /**
0833: * Draws the category labels and returns the updated axis state.
0834: *
0835: * @param g2 the graphics device (<code>null</code> not permitted).
0836: * @param dataArea the area inside the axes (<code>null</code> not
0837: * permitted).
0838: * @param edge the axis location (<code>null</code> not permitted).
0839: * @param state the axis state (<code>null</code> not permitted).
0840: * @param plotState collects information about the plot (<code>null</code>
0841: * permitted).
0842: *
0843: * @return The updated axis state (never <code>null</code>).
0844: *
0845: * @deprecated Use {@link #drawCategoryLabels(Graphics2D, Rectangle2D,
0846: * Rectangle2D, RectangleEdge, AxisState, PlotRenderingInfo)}.
0847: */
0848: protected AxisState drawCategoryLabels(Graphics2D g2,
0849: Rectangle2D dataArea, RectangleEdge edge, AxisState state,
0850: PlotRenderingInfo plotState) {
0851:
0852: // this method is deprecated because we really need the plotArea
0853: // when drawing the labels - see bug 1277726
0854: return drawCategoryLabels(g2, dataArea, dataArea, edge, state,
0855: plotState);
0856: }
0857:
0858: /**
0859: * Draws the category labels and returns the updated axis state.
0860: *
0861: * @param g2 the graphics device (<code>null</code> not permitted).
0862: * @param plotArea the plot area (<code>null</code> not permitted).
0863: * @param dataArea the area inside the axes (<code>null</code> not
0864: * permitted).
0865: * @param edge the axis location (<code>null</code> not permitted).
0866: * @param state the axis state (<code>null</code> not permitted).
0867: * @param plotState collects information about the plot (<code>null</code>
0868: * permitted).
0869: *
0870: * @return The updated axis state (never <code>null</code>).
0871: */
0872: protected AxisState drawCategoryLabels(Graphics2D g2,
0873: Rectangle2D plotArea, Rectangle2D dataArea,
0874: RectangleEdge edge, AxisState state,
0875: PlotRenderingInfo plotState) {
0876:
0877: if (state == null) {
0878: throw new IllegalArgumentException("Null 'state' argument.");
0879: }
0880:
0881: if (isTickLabelsVisible()) {
0882: List ticks = refreshTicks(g2, state, plotArea, edge);
0883: state.setTicks(ticks);
0884:
0885: int categoryIndex = 0;
0886: Iterator iterator = ticks.iterator();
0887: while (iterator.hasNext()) {
0888:
0889: CategoryTick tick = (CategoryTick) iterator.next();
0890: g2.setFont(getTickLabelFont(tick.getCategory()));
0891: g2.setPaint(getTickLabelPaint(tick.getCategory()));
0892:
0893: CategoryLabelPosition position = this .categoryLabelPositions
0894: .getLabelPosition(edge);
0895: double x0 = 0.0;
0896: double x1 = 0.0;
0897: double y0 = 0.0;
0898: double y1 = 0.0;
0899: if (edge == RectangleEdge.TOP) {
0900: x0 = getCategoryStart(categoryIndex, ticks.size(),
0901: dataArea, edge);
0902: x1 = getCategoryEnd(categoryIndex, ticks.size(),
0903: dataArea, edge);
0904: y1 = state.getCursor()
0905: - this .categoryLabelPositionOffset;
0906: y0 = y1 - state.getMax();
0907: } else if (edge == RectangleEdge.BOTTOM) {
0908: x0 = getCategoryStart(categoryIndex, ticks.size(),
0909: dataArea, edge);
0910: x1 = getCategoryEnd(categoryIndex, ticks.size(),
0911: dataArea, edge);
0912: y0 = state.getCursor()
0913: + this .categoryLabelPositionOffset;
0914: y1 = y0 + state.getMax();
0915: } else if (edge == RectangleEdge.LEFT) {
0916: y0 = getCategoryStart(categoryIndex, ticks.size(),
0917: dataArea, edge);
0918: y1 = getCategoryEnd(categoryIndex, ticks.size(),
0919: dataArea, edge);
0920: x1 = state.getCursor()
0921: - this .categoryLabelPositionOffset;
0922: x0 = x1 - state.getMax();
0923: } else if (edge == RectangleEdge.RIGHT) {
0924: y0 = getCategoryStart(categoryIndex, ticks.size(),
0925: dataArea, edge);
0926: y1 = getCategoryEnd(categoryIndex, ticks.size(),
0927: dataArea, edge);
0928: x0 = state.getCursor()
0929: + this .categoryLabelPositionOffset;
0930: x1 = x0 - state.getMax();
0931: }
0932: Rectangle2D area = new Rectangle2D.Double(x0, y0,
0933: (x1 - x0), (y1 - y0));
0934: Point2D anchorPoint = RectangleAnchor.coordinates(area,
0935: position.getCategoryAnchor());
0936: TextBlock block = tick.getLabel();
0937: block.draw(g2, (float) anchorPoint.getX(),
0938: (float) anchorPoint.getY(), position
0939: .getLabelAnchor(), (float) anchorPoint
0940: .getX(), (float) anchorPoint.getY(),
0941: position.getAngle());
0942: Shape bounds = block.calculateBounds(g2,
0943: (float) anchorPoint.getX(), (float) anchorPoint
0944: .getY(), position.getLabelAnchor(),
0945: (float) anchorPoint.getX(), (float) anchorPoint
0946: .getY(), position.getAngle());
0947: if (plotState != null && plotState.getOwner() != null) {
0948: EntityCollection entities = plotState.getOwner()
0949: .getEntityCollection();
0950: if (entities != null) {
0951: String tooltip = getCategoryLabelToolTip(tick
0952: .getCategory());
0953: entities.add(new CategoryLabelEntity(tick
0954: .getCategory(), bounds, tooltip, null));
0955: }
0956: }
0957: categoryIndex++;
0958: }
0959:
0960: if (edge.equals(RectangleEdge.TOP)) {
0961: double h = state.getMax()
0962: + this .categoryLabelPositionOffset;
0963: state.cursorUp(h);
0964: } else if (edge.equals(RectangleEdge.BOTTOM)) {
0965: double h = state.getMax()
0966: + this .categoryLabelPositionOffset;
0967: state.cursorDown(h);
0968: } else if (edge == RectangleEdge.LEFT) {
0969: double w = state.getMax()
0970: + this .categoryLabelPositionOffset;
0971: state.cursorLeft(w);
0972: } else if (edge == RectangleEdge.RIGHT) {
0973: double w = state.getMax()
0974: + this .categoryLabelPositionOffset;
0975: state.cursorRight(w);
0976: }
0977: }
0978: return state;
0979: }
0980:
0981: /**
0982: * Creates a temporary list of ticks that can be used when drawing the axis.
0983: *
0984: * @param g2 the graphics device (used to get font measurements).
0985: * @param state the axis state.
0986: * @param dataArea the area inside the axes.
0987: * @param edge the location of the axis.
0988: *
0989: * @return A list of ticks.
0990: */
0991: public List refreshTicks(Graphics2D g2, AxisState state,
0992: Rectangle2D dataArea, RectangleEdge edge) {
0993:
0994: List ticks = new java.util.ArrayList();
0995:
0996: // sanity check for data area...
0997: if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
0998: return ticks;
0999: }
1000:
1001: CategoryPlot plot = (CategoryPlot) getPlot();
1002: List categories = plot.getCategoriesForAxis(this );
1003: double max = 0.0;
1004:
1005: if (categories != null) {
1006: CategoryLabelPosition position = this .categoryLabelPositions
1007: .getLabelPosition(edge);
1008: float r = this .maximumCategoryLabelWidthRatio;
1009: if (r <= 0.0) {
1010: r = position.getWidthRatio();
1011: }
1012:
1013: float l = 0.0f;
1014: if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {
1015: l = (float) calculateCategorySize(categories.size(),
1016: dataArea, edge);
1017: } else {
1018: if (RectangleEdge.isLeftOrRight(edge)) {
1019: l = (float) dataArea.getWidth();
1020: } else {
1021: l = (float) dataArea.getHeight();
1022: }
1023: }
1024: int categoryIndex = 0;
1025: Iterator iterator = categories.iterator();
1026: while (iterator.hasNext()) {
1027: Comparable category = (Comparable) iterator.next();
1028: TextBlock label = createLabel(category, l * r, edge, g2);
1029: if (edge == RectangleEdge.TOP
1030: || edge == RectangleEdge.BOTTOM) {
1031: max = Math.max(max, calculateTextBlockHeight(label,
1032: position, g2));
1033: } else if (edge == RectangleEdge.LEFT
1034: || edge == RectangleEdge.RIGHT) {
1035: max = Math.max(max, calculateTextBlockWidth(label,
1036: position, g2));
1037: }
1038: Tick tick = new CategoryTick(category, label, position
1039: .getLabelAnchor(),
1040: position.getRotationAnchor(), position
1041: .getAngle());
1042: ticks.add(tick);
1043: categoryIndex = categoryIndex + 1;
1044: }
1045: }
1046: state.setMax(max);
1047: return ticks;
1048:
1049: }
1050:
1051: /**
1052: * Creates a label.
1053: *
1054: * @param category the category.
1055: * @param width the available width.
1056: * @param edge the edge on which the axis appears.
1057: * @param g2 the graphics device.
1058: *
1059: * @return A label.
1060: */
1061: protected TextBlock createLabel(Comparable category, float width,
1062: RectangleEdge edge, Graphics2D g2) {
1063: TextBlock label = TextUtilities.createTextBlock(category
1064: .toString(), getTickLabelFont(category),
1065: getTickLabelPaint(category), width,
1066: this .maximumCategoryLabelLines, new G2TextMeasurer(g2));
1067: return label;
1068: }
1069:
1070: /**
1071: * A utility method for determining the width of a text block.
1072: *
1073: * @param block the text block.
1074: * @param position the position.
1075: * @param g2 the graphics device.
1076: *
1077: * @return The width.
1078: */
1079: protected double calculateTextBlockWidth(TextBlock block,
1080: CategoryLabelPosition position, Graphics2D g2) {
1081:
1082: RectangleInsets insets = getTickLabelInsets();
1083: Size2D size = block.calculateDimensions(g2);
1084: Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size
1085: .getWidth(), size.getHeight());
1086: Shape rotatedBox = ShapeUtilities.rotateShape(box, position
1087: .getAngle(), 0.0f, 0.0f);
1088: double w = rotatedBox.getBounds2D().getWidth()
1089: + insets.getTop() + insets.getBottom();
1090: return w;
1091:
1092: }
1093:
1094: /**
1095: * A utility method for determining the height of a text block.
1096: *
1097: * @param block the text block.
1098: * @param position the label position.
1099: * @param g2 the graphics device.
1100: *
1101: * @return The height.
1102: */
1103: protected double calculateTextBlockHeight(TextBlock block,
1104: CategoryLabelPosition position, Graphics2D g2) {
1105:
1106: RectangleInsets insets = getTickLabelInsets();
1107: Size2D size = block.calculateDimensions(g2);
1108: Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size
1109: .getWidth(), size.getHeight());
1110: Shape rotatedBox = ShapeUtilities.rotateShape(box, position
1111: .getAngle(), 0.0f, 0.0f);
1112: double h = rotatedBox.getBounds2D().getHeight()
1113: + insets.getTop() + insets.getBottom();
1114: return h;
1115:
1116: }
1117:
1118: /**
1119: * Creates a clone of the axis.
1120: *
1121: * @return A clone.
1122: *
1123: * @throws CloneNotSupportedException if some component of the axis does
1124: * not support cloning.
1125: */
1126: public Object clone() throws CloneNotSupportedException {
1127: CategoryAxis clone = (CategoryAxis) super .clone();
1128: clone.tickLabelFontMap = new HashMap(this .tickLabelFontMap);
1129: clone.tickLabelPaintMap = new HashMap(this .tickLabelPaintMap);
1130: clone.categoryLabelToolTips = new HashMap(
1131: this .categoryLabelToolTips);
1132: return clone;
1133: }
1134:
1135: /**
1136: * Tests this axis for equality with an arbitrary object.
1137: *
1138: * @param obj the object (<code>null</code> permitted).
1139: *
1140: * @return A boolean.
1141: */
1142: public boolean equals(Object obj) {
1143: if (obj == this ) {
1144: return true;
1145: }
1146: if (!(obj instanceof CategoryAxis)) {
1147: return false;
1148: }
1149: if (!super .equals(obj)) {
1150: return false;
1151: }
1152: CategoryAxis that = (CategoryAxis) obj;
1153: if (that.lowerMargin != this .lowerMargin) {
1154: return false;
1155: }
1156: if (that.upperMargin != this .upperMargin) {
1157: return false;
1158: }
1159: if (that.categoryMargin != this .categoryMargin) {
1160: return false;
1161: }
1162: if (that.maximumCategoryLabelWidthRatio != this .maximumCategoryLabelWidthRatio) {
1163: return false;
1164: }
1165: if (that.categoryLabelPositionOffset != this .categoryLabelPositionOffset) {
1166: return false;
1167: }
1168: if (!ObjectUtilities.equal(that.categoryLabelPositions,
1169: this .categoryLabelPositions)) {
1170: return false;
1171: }
1172: if (!ObjectUtilities.equal(that.categoryLabelToolTips,
1173: this .categoryLabelToolTips)) {
1174: return false;
1175: }
1176: if (!ObjectUtilities.equal(this .tickLabelFontMap,
1177: that.tickLabelFontMap)) {
1178: return false;
1179: }
1180: if (!equalPaintMaps(this .tickLabelPaintMap,
1181: that.tickLabelPaintMap)) {
1182: return false;
1183: }
1184: return true;
1185: }
1186:
1187: /**
1188: * Returns a hash code for this object.
1189: *
1190: * @return A hash code.
1191: */
1192: public int hashCode() {
1193: if (getLabel() != null) {
1194: return getLabel().hashCode();
1195: } else {
1196: return 0;
1197: }
1198: }
1199:
1200: /**
1201: * Provides serialization support.
1202: *
1203: * @param stream the output stream.
1204: *
1205: * @throws IOException if there is an I/O error.
1206: */
1207: private void writeObject(ObjectOutputStream stream)
1208: throws IOException {
1209: stream.defaultWriteObject();
1210: writePaintMap(this .tickLabelPaintMap, stream);
1211: }
1212:
1213: /**
1214: * Provides serialization support.
1215: *
1216: * @param stream the input stream.
1217: *
1218: * @throws IOException if there is an I/O error.
1219: * @throws ClassNotFoundException if there is a classpath problem.
1220: */
1221: private void readObject(ObjectInputStream stream)
1222: throws IOException, ClassNotFoundException {
1223: stream.defaultReadObject();
1224: this .tickLabelPaintMap = readPaintMap(stream);
1225: }
1226:
1227: /**
1228: * Reads a <code>Map</code> of (<code>Comparable</code>, <code>Paint</code>)
1229: * elements from a stream.
1230: *
1231: * @param in the input stream.
1232: *
1233: * @return The map.
1234: *
1235: * @throws IOException
1236: * @throws ClassNotFoundException
1237: *
1238: * @see #writePaintMap(Map, ObjectOutputStream)
1239: */
1240: private Map readPaintMap(ObjectInputStream in) throws IOException,
1241: ClassNotFoundException {
1242: boolean isNull = in.readBoolean();
1243: if (isNull) {
1244: return null;
1245: }
1246: Map result = new HashMap();
1247: int count = in.readInt();
1248: for (int i = 0; i < count; i++) {
1249: Comparable category = (Comparable) in.readObject();
1250: Paint paint = SerialUtilities.readPaint(in);
1251: result.put(category, paint);
1252: }
1253: return result;
1254: }
1255:
1256: /**
1257: * Writes a map of (<code>Comparable</code>, <code>Paint</code>)
1258: * elements to a stream.
1259: *
1260: * @param map the map (<code>null</code> permitted).
1261: *
1262: * @param out
1263: * @throws IOException
1264: *
1265: * @see #readPaintMap(ObjectInputStream)
1266: */
1267: private void writePaintMap(Map map, ObjectOutputStream out)
1268: throws IOException {
1269: if (map == null) {
1270: out.writeBoolean(true);
1271: } else {
1272: out.writeBoolean(false);
1273: Set keys = map.keySet();
1274: int count = keys.size();
1275: out.writeInt(count);
1276: Iterator iterator = keys.iterator();
1277: while (iterator.hasNext()) {
1278: Comparable key = (Comparable) iterator.next();
1279: out.writeObject(key);
1280: SerialUtilities.writePaint((Paint) map.get(key), out);
1281: }
1282: }
1283: }
1284:
1285: /**
1286: * Tests two maps containing (<code>Comparable</code>, <code>Paint</code>)
1287: * elements for equality.
1288: *
1289: * @param map1 the first map (<code>null</code> not permitted).
1290: * @param map2 the second map (<code>null</code> not permitted).
1291: *
1292: * @return A boolean.
1293: */
1294: private boolean equalPaintMaps(Map map1, Map map2) {
1295: if (map1.size() != map2.size()) {
1296: return false;
1297: }
1298: Set keys = map1.keySet();
1299: Iterator iterator = keys.iterator();
1300: while (iterator.hasNext()) {
1301: Comparable key = (Comparable) iterator.next();
1302: Paint p1 = (Paint) map1.get(key);
1303: Paint p2 = (Paint) map2.get(key);
1304: if (!PaintUtilities.equal(p1, p2)) {
1305: return false;
1306: }
1307: }
1308: return true;
1309: }
1310:
1311: }
|