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-2006, 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-2006, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert;
  33:  * Contributor(s):   Adriaan Joubert;
  34:  *
  35:  * $Id: SubCategoryAxis.java,v 1.6.2.2 2006/08/18 14:48:31 mungady Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 12-May-2004 : Version 1 (DG);
  40:  * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities 
  41:  *               --> TextUtilities (DG);
  42:  * 26-Apr-2005 : Removed logger (DG);
  43:  * ------------- JFREECHART 1.0.0 ---------------------------------------------
  44:  * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
  45:  *               Joubert (1277726) (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.
 104:      * 
 105:      * @param subCategory  the sub-category.
 106:      */
 107:     public void addSubCategory(Comparable subCategory) {
 108:         this.subCategories.add(subCategory);    
 109:     }
 110:     
 111:     /**
 112:      * Returns the font used to display the sub-category labels.
 113:      * 
 114:      * @return The font (never <code>null</code>).
 115:      */
 116:     public Font getSubLabelFont() {
 117:         return this.subLabelFont;   
 118:     }
 119:     
 120:     /**
 121:      * Sets the font used to display the sub-category labels and sends an 
 122:      * {@link AxisChangeEvent} to all registered listeners.
 123:      * 
 124:      * @param font  the font (<code>null</code> not permitted).
 125:      */
 126:     public void setSubLabelFont(Font font) {
 127:         if (font == null) {
 128:             throw new IllegalArgumentException("Null 'font' argument.");   
 129:         }
 130:         this.subLabelFont = font;
 131:         notifyListeners(new AxisChangeEvent(this));
 132:     }
 133:     
 134:     /**
 135:      * Returns the paint used to display the sub-category labels.
 136:      * 
 137:      * @return The paint (never <code>null</code>).
 138:      */
 139:     public Paint getSubLabelPaint() {
 140:         return this.subLabelPaint;   
 141:     }
 142:     
 143:     /**
 144:      * Sets the paint used to display the sub-category labels and sends an 
 145:      * {@link AxisChangeEvent} to all registered listeners.
 146:      * 
 147:      * @param paint  the paint (<code>null</code> not permitted).
 148:      */
 149:     public void setSubLabelPaint(Paint paint) {
 150:         if (paint == null) {
 151:             throw new IllegalArgumentException("Null 'paint' argument.");   
 152:         }
 153:         this.subLabelPaint = paint;
 154:         notifyListeners(new AxisChangeEvent(this));
 155:     }
 156:     
 157:     /**
 158:      * Estimates the space required for the axis, given a specific drawing area.
 159:      *
 160:      * @param g2  the graphics device (used to obtain font information).
 161:      * @param plot  the plot that the axis belongs to.
 162:      * @param plotArea  the area within which the axis should be drawn.
 163:      * @param edge  the axis location (top or bottom).
 164:      * @param space  the space already reserved.
 165:      *
 166:      * @return The space required to draw the axis.
 167:      */
 168:     public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 
 169:                                   Rectangle2D plotArea, 
 170:                                   RectangleEdge edge, AxisSpace space) {
 171: 
 172:         // create a new space object if one wasn't supplied...
 173:         if (space == null) {
 174:             space = new AxisSpace();
 175:         }
 176:         
 177:         // if the axis is not visible, no additional space is required...
 178:         if (!isVisible()) {
 179:             return space;
 180:         }
 181: 
 182:         space = super.reserveSpace(g2, plot, plotArea, edge, space);
 183:         double maxdim = getMaxDim(g2, edge);
 184:         if (RectangleEdge.isTopOrBottom(edge)) {
 185:             space.add(maxdim, edge);
 186:         }
 187:         else if (RectangleEdge.isLeftOrRight(edge)) {
 188:             space.add(maxdim, edge);
 189:         }
 190:         return space;
 191:     }
 192:     
 193:     /**
 194:      * Returns the maximum of the relevant dimension (height or width) of the 
 195:      * subcategory labels.
 196:      * 
 197:      * @param g2  the graphics device.
 198:      * @param edge  the edge.
 199:      * 
 200:      * @return The maximum dimension.
 201:      */
 202:     private double getMaxDim(Graphics2D g2, RectangleEdge edge) {
 203:         double result = 0.0;
 204:         g2.setFont(this.subLabelFont);
 205:         FontMetrics fm = g2.getFontMetrics();
 206:         Iterator iterator = this.subCategories.iterator();
 207:         while (iterator.hasNext()) {
 208:             Comparable subcategory = (Comparable) iterator.next();
 209:             String label = subcategory.toString();
 210:             Rectangle2D bounds = TextUtilities.getTextBounds(label, g2, fm);
 211:             double dim = 0.0;
 212:             if (RectangleEdge.isLeftOrRight(edge)) {
 213:                 dim = bounds.getWidth();   
 214:             }
 215:             else {  // must be top or bottom
 216:                 dim = bounds.getHeight();
 217:             }
 218:             result = Math.max(result, dim);
 219:         }   
 220:         return result;
 221:     }
 222:     
 223:     /**
 224:      * Draws the axis on a Java 2D graphics device (such as the screen or a 
 225:      * printer).
 226:      *
 227:      * @param g2  the graphics device (<code>null</code> not permitted).
 228:      * @param cursor  the cursor location.
 229:      * @param plotArea  the area within which the axis should be drawn 
 230:      *                  (<code>null</code> not permitted).
 231:      * @param dataArea  the area within which the plot is being drawn 
 232:      *                  (<code>null</code> not permitted).
 233:      * @param edge  the location of the axis (<code>null</code> not permitted).
 234:      * @param plotState  collects information about the plot 
 235:      *                   (<code>null</code> permitted).
 236:      * 
 237:      * @return The axis state (never <code>null</code>).
 238:      */
 239:     public AxisState draw(Graphics2D g2, 
 240:                           double cursor, 
 241:                           Rectangle2D plotArea, 
 242:                           Rectangle2D dataArea,
 243:                           RectangleEdge edge,
 244:                           PlotRenderingInfo plotState) {
 245:         
 246:         // if the axis is not visible, don't draw it...
 247:         if (!isVisible()) {
 248:             return new AxisState(cursor);
 249:         }
 250:         
 251:         if (isAxisLineVisible()) {
 252:             drawAxisLine(g2, cursor, dataArea, edge);
 253:         }
 254: 
 255:         // draw the category labels and axis label
 256:         AxisState state = new AxisState(cursor);
 257:         state = drawSubCategoryLabels(
 258:             g2, plotArea, dataArea, edge, state, plotState
 259:         );
 260:         state = drawCategoryLabels(g2, plotArea, dataArea, edge, state, 
 261:                 plotState);
 262:         state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
 263:     
 264:         return state;
 265: 
 266:     }
 267:     
 268:     /**
 269:      * Draws the category labels and returns the updated axis state.
 270:      *
 271:      * @param g2  the graphics device (<code>null</code> not permitted).
 272:      * @param plotArea  the plot area (<code>null</code> not permitted).
 273:      * @param dataArea  the area inside the axes (<code>null</code> not 
 274:      *                  permitted).
 275:      * @param edge  the axis location (<code>null</code> not permitted).
 276:      * @param state  the axis state (<code>null</code> not permitted).
 277:      * @param plotState  collects information about the plot (<code>null</code> 
 278:      *                   permitted).
 279:      * 
 280:      * @return The updated axis state (never <code>null</code>).
 281:      */
 282:     protected AxisState drawSubCategoryLabels(Graphics2D g2,
 283:                                               Rectangle2D plotArea,
 284:                                               Rectangle2D dataArea,
 285:                                               RectangleEdge edge,
 286:                                               AxisState state,
 287:                                               PlotRenderingInfo plotState) {
 288: 
 289:         if (state == null) {
 290:             throw new IllegalArgumentException("Null 'state' argument.");
 291:         }
 292: 
 293:         g2.setFont(this.subLabelFont);
 294:         g2.setPaint(this.subLabelPaint);
 295:         CategoryPlot plot = (CategoryPlot) getPlot();
 296:         CategoryDataset dataset = plot.getDataset();
 297:         int categoryCount = dataset.getColumnCount();
 298: 
 299:         double maxdim = getMaxDim(g2, edge);
 300:         for (int categoryIndex = 0; categoryIndex < categoryCount; 
 301:              categoryIndex++) {
 302: 
 303:             double x0 = 0.0;
 304:             double x1 = 0.0;
 305:             double y0 = 0.0;
 306:             double y1 = 0.0;
 307:             if (edge == RectangleEdge.TOP) {
 308:                 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 309:                         edge);
 310:                 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 311:                         edge);
 312:                 y1 = state.getCursor();
 313:                 y0 = y1 - maxdim;
 314:             }
 315:             else if (edge == RectangleEdge.BOTTOM) {
 316:                 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 317:                         edge);
 318:                 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 319:                         edge); 
 320:                 y0 = state.getCursor();                   
 321:                 y1 = y0 + maxdim;
 322:             }
 323:             else if (edge == RectangleEdge.LEFT) {
 324:                 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 325:                         edge);
 326:                 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 327:                         edge);
 328:                 x1 = state.getCursor();
 329:                 x0 = x1 - maxdim;
 330:             }
 331:             else if (edge == RectangleEdge.RIGHT) {
 332:                 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
 333:                         edge);
 334:                 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
 335:                         edge);
 336:                 x0 = state.getCursor();
 337:                 x1 = x0 + maxdim;
 338:             }
 339:             Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), 
 340:                     (y1 - y0));
 341:             int subCategoryCount = this.subCategories.size();
 342:             float width = (float) ((x1 - x0) / subCategoryCount);
 343:             float height = (float) ((y1 - y0) / subCategoryCount);
 344:             float xx = 0.0f;
 345:             float yy = 0.0f;
 346:             for (int i = 0; i < subCategoryCount; i++) {
 347:                 if (RectangleEdge.isTopOrBottom(edge)) {
 348:                     xx = (float) (x0 + (i + 0.5) * width);
 349:                     yy = (float) area.getCenterY();
 350:                 }
 351:                 else {
 352:                     xx = (float) area.getCenterX();
 353:                     yy = (float) (y0 + (i + 0.5) * height);                   
 354:                 }
 355:                 String label = this.subCategories.get(i).toString();
 356:                 TextUtilities.drawRotatedString(label, g2, xx, yy, 
 357:                         TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
 358:             }
 359:         }
 360: 
 361:         if (edge.equals(RectangleEdge.TOP)) {
 362:             double h = maxdim;
 363:             state.cursorUp(h);
 364:         }
 365:         else if (edge.equals(RectangleEdge.BOTTOM)) {
 366:             double h = maxdim;
 367:             state.cursorDown(h);
 368:         }
 369:         else if (edge == RectangleEdge.LEFT) {
 370:             double w = maxdim;
 371:             state.cursorLeft(w);
 372:         }
 373:         else if (edge == RectangleEdge.RIGHT) {
 374:             double w = maxdim;
 375:             state.cursorRight(w);
 376:         }
 377:         return state;
 378:     }
 379:     
 380:     /**
 381:      * Tests the axis for equality with an arbitrary object.
 382:      * 
 383:      * @param obj  the object (<code>null</code> permitted).
 384:      * 
 385:      * @return A boolean.
 386:      */
 387:     public boolean equals(Object obj) {
 388:         if (obj == this) {
 389:             return true;
 390:         }
 391:         if (obj instanceof SubCategoryAxis && super.equals(obj)) {
 392:             SubCategoryAxis axis = (SubCategoryAxis) obj;
 393:             if (!this.subCategories.equals(axis.subCategories)) {
 394:                 return false;
 395:             }
 396:             if (!this.subLabelFont.equals(axis.subLabelFont)) {
 397:                 return false;   
 398:             }
 399:             if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
 400:                 return false;   
 401:             }
 402:             return true;
 403:         }
 404:         return false;        
 405:     }
 406:     
 407:     /**
 408:      * Provides serialization support.
 409:      *
 410:      * @param stream  the output stream.
 411:      *
 412:      * @throws IOException  if there is an I/O error.
 413:      */
 414:     private void writeObject(ObjectOutputStream stream) throws IOException {
 415:         stream.defaultWriteObject();
 416:         SerialUtilities.writePaint(this.subLabelPaint, stream);
 417:     }
 418: 
 419:     /**
 420:      * Provides serialization support.
 421:      *
 422:      * @param stream  the input stream.
 423:      *
 424:      * @throws IOException  if there is an I/O error.
 425:      * @throws ClassNotFoundException  if there is a classpath problem.
 426:      */
 427:     private void readObject(ObjectInputStream stream) 
 428:         throws IOException, ClassNotFoundException {
 429:         stream.defaultReadObject();
 430:         this.subLabelPaint = SerialUtilities.readPaint(stream);
 431:     }
 432:   
 433: }