Source for org.jfree.chart.axis.SubCategoryAxis

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * --------------------
  28:  * SubCategoryAxis.java
  29:  * --------------------
  30:  * (C) Copyright 2004-2007, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert;
  33:  * Contributor(s):   Adriaan Joubert;
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 12-May-2004 : Version 1 (DG);
  38:  * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities 
  39:  *               --> TextUtilities (DG);
  40:  * 26-Apr-2005 : Removed logger (DG);
  41:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  42:  * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
  43:  *               Joubert (1277726) (DG);
  44:  * 30-May-2007 : Added argument check and event notification to 
  45:  *               addSubCategory() (DG);
  46:  *
  47:  */
  48: 
  49: package org.jfree.chart.axis;
  50: 
  51: import java.awt.Color;
  52: import java.awt.Font;
  53: import java.awt.FontMetrics;
  54: import java.awt.Graphics2D;
  55: import java.awt.Paint;
  56: import java.awt.geom.Rectangle2D;
  57: import java.io.IOException;
  58: import java.io.ObjectInputStream;
  59: import java.io.ObjectOutputStream;
  60: import java.io.Serializable;
  61: import java.util.Iterator;
  62: import java.util.List;
  63: 
  64: import org.jfree.chart.event.AxisChangeEvent;
  65: import org.jfree.chart.plot.CategoryPlot;
  66: import org.jfree.chart.plot.Plot;
  67: import org.jfree.chart.plot.PlotRenderingInfo;
  68: import org.jfree.data.category.CategoryDataset;
  69: import org.jfree.io.SerialUtilities;
  70: import org.jfree.text.TextUtilities;
  71: import org.jfree.ui.RectangleEdge;
  72: import org.jfree.ui.TextAnchor;
  73: 
  74: /**
  75:  * A specialised category axis that can display sub-categories.
  76:  */
  77: public class SubCategoryAxis extends CategoryAxis 
  78:                              implements Cloneable, Serializable {
  79:     
  80:     /** For serialization. */
  81:     private static final long serialVersionUID = -1279463299793228344L;
  82:     
  83:     /** Storage for the sub-categories (these need to be set manually). */
  84:     private List subCategories;
  85:     
  86:     /** The font for the sub-category labels. */
  87:     private Font subLabelFont = new Font("SansSerif", Font.PLAIN, 10);
  88:     
  89:     /** The paint for the sub-category labels. */
  90:     private transient Paint subLabelPaint = Color.black;
  91:     
  92:     /**
  93:      * Creates a new axis.
  94:      * 
  95:      * @param label  the axis label.
  96:      */
  97:     public SubCategoryAxis(String label) {
  98:         super(label);
  99:         this.subCategories = new java.util.ArrayList();
 100:     }
 101: 
 102:     /**
 103:      * Adds a sub-category to the axis and sends an {@link AxisChangeEvent} to
 104:      * all registered listeners.
 105:      * 
 106:      * @param subCategory  the sub-category (<code>null</code> not permitted).
 107:      */
 108:     public void addSubCategory(Comparable subCategory) {
 109:         if (subCategory == null) {
 110:             throw new IllegalArgumentException("Null 'subcategory' axis.");
 111:         }
 112:         this.subCategories.add(subCategory);
 113:         notifyListeners(new AxisChangeEvent(this));        
 114:     }
 115:     
 116:     /**
 117:      * Returns the font used to display the sub-category labels.
 118:      * 
 119:      * @return The font (never <code>null</code>).
 120:      * 
 121:      * @see #setSubLabelFont(Font)
 122:      */
 123:     public Font getSubLabelFont() {
 124:         return this.subLabelFont;   
 125:     }
 126:     
 127:     /**
 128:      * Sets the font used to display the sub-category labels and sends an 
 129:      * {@link AxisChangeEvent} to all registered listeners.
 130:      * 
 131:      * @param font  the font (<code>null</code> not permitted).
 132:      * 
 133:      * @see #getSubLabelFont()
 134:      */
 135:     public void setSubLabelFont(Font font) {
 136:         if (font == null) {
 137:             throw new IllegalArgumentException("Null 'font' argument.");   
 138:         }
 139:         this.subLabelFont = font;
 140:         notifyListeners(new AxisChangeEvent(this));
 141:     }
 142:     
 143:     /**
 144:      * Returns the paint used to display the sub-category labels.
 145:      * 
 146:      * @return The paint (never <code>null</code>).
 147:      * 
 148:      * @see #setSubLabelPaint(Paint)
 149:      */
 150:     public Paint getSubLabelPaint() {
 151:         return this.subLabelPaint;   
 152:     }
 153:     
 154:     /**
 155:      * Sets the paint used to display the sub-category labels and sends an 
 156:      * {@link AxisChangeEvent} to all registered listeners.
 157:      * 
 158:      * @param paint  the paint (<code>null</code> not permitted).
 159:      * 
 160:      * @see #getSubLabelPaint()
 161:      */
 162:     public void setSubLabelPaint(Paint paint) {
 163:         if (paint == null) {
 164:             throw new IllegalArgumentException("Null 'paint' argument.");   
 165:         }
 166:         this.subLabelPaint = paint;
 167:         notifyListeners(new AxisChangeEvent(this));
 168:     }
 169:     
 170:     /**
 171:      * Estimates the space required for the axis, given a specific drawing area.
 172:      *
 173:      * @param g2  the graphics device (used to obtain font information).
 174:      * @param plot  the plot that the axis belongs to.
 175:      * @param plotArea  the area within which the axis should be drawn.
 176:      * @param edge  the axis location (top or bottom).
 177:      * @param space  the space already reserved.
 178:      *
 179:      * @return The space required to draw the axis.
 180:      */
 181:     public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 
 182:                                   Rectangle2D plotArea, 
 183:                                   RectangleEdge edge, AxisSpace space) {
 184: 
 185:         // create a new space object if one wasn't supplied...
 186:         if (space == null) {
 187:             space = new AxisSpace();
 188:         }
 189:         
 190:         // if the axis is not visible, no additional space is required...
 191:         if (!isVisible()) {
 192:             return space;
 193:         }
 194: 
 195:         space = super.reserveSpace(g2, plot, plotArea, edge, space);
 196:         double maxdim = getMaxDim(g2, edge);
 197:         if (RectangleEdge.isTopOrBottom(edge)) {
 198:             space.add(maxdim, edge);
 199:         }
 200:         else if (RectangleEdge.isLeftOrRight(edge)) {
 201:             space.add(maxdim, edge);
 202:         }
 203:         return space;
 204:     }
 205:     
 206:     /**
 207:      * Returns the maximum of the relevant dimension (height or width) of the 
 208:      * subcategory labels.
 209:      * 
 210:      * @param g2  the graphics device.
 211:      * @param edge  the edge.
 212:      * 
 213:      * @return The maximum dimension.
 214:      */
 215:     private double getMaxDim(Graphics2D g2, RectangleEdge edge) {
 216:         double result = 0.0;
 217:         g2.setFont(this.subLabelFont);
 218:         FontMetrics fm = g2.getFontMetrics();
 219:         Iterator iterator = this.subCategories.iterator();
 220:         while (iterator.hasNext()) {
 221:             Comparable subcategory = (Comparable) iterator.next();
 222:             String label = subcategory.toString();
 223:             Rectangle2D bounds = TextUtilities.getTextBounds(label, g2, fm);
 224:             double dim = 0.0;
 225:             if (RectangleEdge.isLeftOrRight(edge)) {
 226:                 dim = bounds.getWidth();   
 227:             }
 228:             else {  // must be top or bottom
 229:                 dim = bounds.getHeight();
 230:             }
 231:             result = Math.max(result, dim);
 232:         }   
 233:         return result;
 234:     }
 235:     
 236:     /**
 237:      * Draws the axis on a Java 2D graphics device (such as the screen or a 
 238:      * printer).
 239:      *
 240:      * @param g2  the graphics device (<code>null</code> not permitted).
 241:      * @param cursor  the cursor location.
 242:      * @param plotArea  the area within which the axis should be drawn 
 243:      *                  (<code>null</code> not permitted).
 244:      * @param dataArea  the area within which the plot is being drawn 
 245:      *                  (<code>null</code> not permitted).
 246:      * @param edge  the location of the axis (<code>null</code> not permitted).
 247:      * @param plotState  collects information about the plot 
 248:      *                   (<code>null</code> permitted).
 249:      * 
 250:      * @return The axis state (never <code>null</code>).
 251:      */
 252:     public AxisState draw(Graphics2D g2, 
 253:                           double cursor, 
 254:                           Rectangle2D plotArea, 
 255:                           Rectangle2D dataArea,
 256:                           RectangleEdge edge,
 257:                           PlotRenderingInfo plotState) {
 258:         
 259:         // if the axis is not visible, don't draw it...
 260:         if (!isVisible()) {
 261:             return new AxisState(cursor);
 262:         }
 263:         
 264:         if (isAxisLineVisible()) {
 265:             drawAxisLine(g2, cursor, dataArea, edge);
 266:         }
 267: 
 268:         // draw the category labels and axis label
 269:         AxisState state = new AxisState(cursor);
 270:         state = drawSubCategoryLabels(
 271:             g2, plotArea, dataArea, edge, state, plotState
 272:         );
 273:         state = drawCategoryLabels(g2, plotArea, dataArea, edge, state, 
 274:                 plotState);
 275:         state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
 276:     
 277:         return state;
 278: 
 279:     }
 280:     
 281:     /**
 282:      * Draws the category labels and returns the updated axis state.
 283:      *
 284:      * @param g2  the graphics device (<code>null</code> not permitted).
 285:      * @param plotArea  the plot area (<code>null</code> not permitted).
 286:      * @param dataArea  the area inside the axes (<code>null</code> not 
 287:      *                  permitted).
 288:      * @param edge  the axis location (<code>null</code> not permitted).
 289:      * @param state  the axis state (<code>null</code> not permitted).
 290:      * @param plotState  collects information about the plot (<code>null</code> 
 291:      *                   permitted).
 292:      * 
 293:      * @return The updated axis state (never <code>null</code>).
 294:      */
 295:     protected AxisState drawSubCategoryLabels(Graphics2D g2,
 296:                                               Rectangle2D plotArea,
 297:                                               Rectangle2D dataArea,
 298:                                               RectangleEdge edge,
 299:                                               AxisState state,
 300:                                               PlotRenderingInfo plotState) {
 301: 
 302:         if (state == null) {
 303:             throw new IllegalArgumentException("Null 'state' argument.");
 304:         }
 305: 
 306:         g2.setFont(this.subLabelFont);
 307:         g2.setPaint(this.subLabelPaint);
 308:         CategoryPlot plot = (CategoryPlot) getPlot();
 309:         CategoryDataset dataset = plot.getDataset();
 310:         int categoryCount = dataset.getColumnCount();
 311: 
 312:         double maxdim = getMaxDim(g2, edge);
 313:         for (int categoryIndex = 0; categoryIndex < categoryCount; 
 314:              categoryIndex++) {
 315: 
 316:             double x0 = 0.0;
 317:             double x1 = 0.0;
 318:             double y0 = 0.0;
 319:             double y1 = 0.0;
 320:             if (edge == RectangleEdge.TOP) {
 321:                 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 322:                         edge);
 323:                 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 324:                         edge);
 325:                 y1 = state.getCursor();
 326:                 y0 = y1 - maxdim;
 327:             }
 328:             else if (edge == RectangleEdge.BOTTOM) {
 329:                 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 330:                         edge);
 331:                 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 332:                         edge); 
 333:                 y0 = state.getCursor();                   
 334:                 y1 = y0 + maxdim;
 335:             }
 336:             else if (edge == RectangleEdge.LEFT) {
 337:                 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 338:                         edge);
 339:                 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 340:                         edge);
 341:                 x1 = state.getCursor();
 342:                 x0 = x1 - maxdim;
 343:             }
 344:             else if (edge == RectangleEdge.RIGHT) {
 345:                 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 346:                         edge);
 347:                 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 348:                         edge);
 349:                 x0 = state.getCursor();
 350:                 x1 = x0 + maxdim;
 351:             }
 352:             Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), 
 353:                     (y1 - y0));
 354:             int subCategoryCount = this.subCategories.size();
 355:             float width = (float) ((x1 - x0) / subCategoryCount);
 356:             float height = (float) ((y1 - y0) / subCategoryCount);
 357:             float xx = 0.0f;
 358:             float yy = 0.0f;
 359:             for (int i = 0; i < subCategoryCount; i++) {
 360:                 if (RectangleEdge.isTopOrBottom(edge)) {
 361:                     xx = (float) (x0 + (i + 0.5) * width);
 362:                     yy = (float) area.getCenterY();
 363:                 }
 364:                 else {
 365:                     xx = (float) area.getCenterX();
 366:                     yy = (float) (y0 + (i + 0.5) * height);                   
 367:                 }
 368:                 String label = this.subCategories.get(i).toString();
 369:                 TextUtilities.drawRotatedString(label, g2, xx, yy, 
 370:                         TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
 371:             }
 372:         }
 373: 
 374:         if (edge.equals(RectangleEdge.TOP)) {
 375:             double h = maxdim;
 376:             state.cursorUp(h);
 377:         }
 378:         else if (edge.equals(RectangleEdge.BOTTOM)) {
 379:             double h = maxdim;
 380:             state.cursorDown(h);
 381:         }
 382:         else if (edge == RectangleEdge.LEFT) {
 383:             double w = maxdim;
 384:             state.cursorLeft(w);
 385:         }
 386:         else if (edge == RectangleEdge.RIGHT) {
 387:             double w = maxdim;
 388:             state.cursorRight(w);
 389:         }
 390:         return state;
 391:     }
 392:     
 393:     /**
 394:      * Tests the axis for equality with an arbitrary object.
 395:      * 
 396:      * @param obj  the object (<code>null</code> permitted).
 397:      * 
 398:      * @return A boolean.
 399:      */
 400:     public boolean equals(Object obj) {
 401:         if (obj == this) {
 402:             return true;
 403:         }
 404:         if (obj instanceof SubCategoryAxis && super.equals(obj)) {
 405:             SubCategoryAxis axis = (SubCategoryAxis) obj;
 406:             if (!this.subCategories.equals(axis.subCategories)) {
 407:                 return false;
 408:             }
 409:             if (!this.subLabelFont.equals(axis.subLabelFont)) {
 410:                 return false;   
 411:             }
 412:             if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
 413:                 return false;   
 414:             }
 415:             return true;
 416:         }
 417:         return false;        
 418:     }
 419:     
 420:     /**
 421:      * Provides serialization support.
 422:      *
 423:      * @param stream  the output stream.
 424:      *
 425:      * @throws IOException  if there is an I/O error.
 426:      */
 427:     private void writeObject(ObjectOutputStream stream) throws IOException {
 428:         stream.defaultWriteObject();
 429:         SerialUtilities.writePaint(this.subLabelPaint, stream);
 430:     }
 431: 
 432:     /**
 433:      * Provides serialization support.
 434:      *
 435:      * @param stream  the input stream.
 436:      *
 437:      * @throws IOException  if there is an I/O error.
 438:      * @throws ClassNotFoundException  if there is a classpath problem.
 439:      */
 440:     private void readObject(ObjectInputStream stream) 
 441:         throws IOException, ClassNotFoundException {
 442:         stream.defaultReadObject();
 443:         this.subLabelPaint = SerialUtilities.readPaint(stream);
 444:     }
 445:   
 446: }