001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, by Object Refinery Limited 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * -------------------- 028 * SubCategoryAxis.java 029 * -------------------- 030 * (C) Copyright 2004-2008, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Adriaan Joubert; 034 * 035 * Changes 036 * ------- 037 * 12-May-2004 : Version 1 (DG); 038 * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities 039 * --> TextUtilities (DG); 040 * 26-Apr-2005 : Removed logger (DG); 041 * ------------- JFREECHART 1.0.x --------------------------------------------- 042 * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan 043 * Joubert (1277726) (DG); 044 * 30-May-2007 : Added argument check and event notification to 045 * addSubCategory() (DG); 046 * 13-Nov-2008 : Fix NullPointerException when dataset is null - see bug 047 * report 2275695 (DG); 048 * 049 */ 050 051 package org.jfree.chart.axis; 052 053 import java.awt.Color; 054 import java.awt.Font; 055 import java.awt.FontMetrics; 056 import java.awt.Graphics2D; 057 import java.awt.Paint; 058 import java.awt.geom.Rectangle2D; 059 import java.io.IOException; 060 import java.io.ObjectInputStream; 061 import java.io.ObjectOutputStream; 062 import java.io.Serializable; 063 import java.util.Iterator; 064 import java.util.List; 065 066 import org.jfree.chart.event.AxisChangeEvent; 067 import org.jfree.chart.plot.CategoryPlot; 068 import org.jfree.chart.plot.Plot; 069 import org.jfree.chart.plot.PlotRenderingInfo; 070 import org.jfree.data.category.CategoryDataset; 071 import org.jfree.io.SerialUtilities; 072 import org.jfree.text.TextUtilities; 073 import org.jfree.ui.RectangleEdge; 074 import org.jfree.ui.TextAnchor; 075 076 /** 077 * A specialised category axis that can display sub-categories. 078 */ 079 public class SubCategoryAxis extends CategoryAxis 080 implements Cloneable, Serializable { 081 082 /** For serialization. */ 083 private static final long serialVersionUID = -1279463299793228344L; 084 085 /** Storage for the sub-categories (these need to be set manually). */ 086 private List subCategories; 087 088 /** The font for the sub-category labels. */ 089 private Font subLabelFont = new Font("SansSerif", Font.PLAIN, 10); 090 091 /** The paint for the sub-category labels. */ 092 private transient Paint subLabelPaint = Color.black; 093 094 /** 095 * Creates a new axis. 096 * 097 * @param label the axis label. 098 */ 099 public SubCategoryAxis(String label) { 100 super(label); 101 this.subCategories = new java.util.ArrayList(); 102 } 103 104 /** 105 * Adds a sub-category to the axis and sends an {@link AxisChangeEvent} to 106 * all registered listeners. 107 * 108 * @param subCategory the sub-category (<code>null</code> not permitted). 109 */ 110 public void addSubCategory(Comparable subCategory) { 111 if (subCategory == null) { 112 throw new IllegalArgumentException("Null 'subcategory' axis."); 113 } 114 this.subCategories.add(subCategory); 115 notifyListeners(new AxisChangeEvent(this)); 116 } 117 118 /** 119 * Returns the font used to display the sub-category labels. 120 * 121 * @return The font (never <code>null</code>). 122 * 123 * @see #setSubLabelFont(Font) 124 */ 125 public Font getSubLabelFont() { 126 return this.subLabelFont; 127 } 128 129 /** 130 * Sets the font used to display the sub-category labels and sends an 131 * {@link AxisChangeEvent} to all registered listeners. 132 * 133 * @param font the font (<code>null</code> not permitted). 134 * 135 * @see #getSubLabelFont() 136 */ 137 public void setSubLabelFont(Font font) { 138 if (font == null) { 139 throw new IllegalArgumentException("Null 'font' argument."); 140 } 141 this.subLabelFont = font; 142 notifyListeners(new AxisChangeEvent(this)); 143 } 144 145 /** 146 * Returns the paint used to display the sub-category labels. 147 * 148 * @return The paint (never <code>null</code>). 149 * 150 * @see #setSubLabelPaint(Paint) 151 */ 152 public Paint getSubLabelPaint() { 153 return this.subLabelPaint; 154 } 155 156 /** 157 * Sets the paint used to display the sub-category labels and sends an 158 * {@link AxisChangeEvent} to all registered listeners. 159 * 160 * @param paint the paint (<code>null</code> not permitted). 161 * 162 * @see #getSubLabelPaint() 163 */ 164 public void setSubLabelPaint(Paint paint) { 165 if (paint == null) { 166 throw new IllegalArgumentException("Null 'paint' argument."); 167 } 168 this.subLabelPaint = paint; 169 notifyListeners(new AxisChangeEvent(this)); 170 } 171 172 /** 173 * Estimates the space required for the axis, given a specific drawing area. 174 * 175 * @param g2 the graphics device (used to obtain font information). 176 * @param plot the plot that the axis belongs to. 177 * @param plotArea the area within which the axis should be drawn. 178 * @param edge the axis location (top or bottom). 179 * @param space the space already reserved. 180 * 181 * @return The space required to draw the axis. 182 */ 183 public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 184 Rectangle2D plotArea, 185 RectangleEdge edge, AxisSpace space) { 186 187 // create a new space object if one wasn't supplied... 188 if (space == null) { 189 space = new AxisSpace(); 190 } 191 192 // if the axis is not visible, no additional space is required... 193 if (!isVisible()) { 194 return space; 195 } 196 197 space = super.reserveSpace(g2, plot, plotArea, edge, space); 198 double maxdim = getMaxDim(g2, edge); 199 if (RectangleEdge.isTopOrBottom(edge)) { 200 space.add(maxdim, edge); 201 } 202 else if (RectangleEdge.isLeftOrRight(edge)) { 203 space.add(maxdim, edge); 204 } 205 return space; 206 } 207 208 /** 209 * Returns the maximum of the relevant dimension (height or width) of the 210 * subcategory labels. 211 * 212 * @param g2 the graphics device. 213 * @param edge the edge. 214 * 215 * @return The maximum dimension. 216 */ 217 private double getMaxDim(Graphics2D g2, RectangleEdge edge) { 218 double result = 0.0; 219 g2.setFont(this.subLabelFont); 220 FontMetrics fm = g2.getFontMetrics(); 221 Iterator iterator = this.subCategories.iterator(); 222 while (iterator.hasNext()) { 223 Comparable subcategory = (Comparable) iterator.next(); 224 String label = subcategory.toString(); 225 Rectangle2D bounds = TextUtilities.getTextBounds(label, g2, fm); 226 double dim = 0.0; 227 if (RectangleEdge.isLeftOrRight(edge)) { 228 dim = bounds.getWidth(); 229 } 230 else { // must be top or bottom 231 dim = bounds.getHeight(); 232 } 233 result = Math.max(result, dim); 234 } 235 return result; 236 } 237 238 /** 239 * Draws the axis on a Java 2D graphics device (such as the screen or a 240 * printer). 241 * 242 * @param g2 the graphics device (<code>null</code> not permitted). 243 * @param cursor the cursor location. 244 * @param plotArea the area within which the axis should be drawn 245 * (<code>null</code> not permitted). 246 * @param dataArea the area within which the plot is being drawn 247 * (<code>null</code> not permitted). 248 * @param edge the location of the axis (<code>null</code> not permitted). 249 * @param plotState collects information about the plot 250 * (<code>null</code> permitted). 251 * 252 * @return The axis state (never <code>null</code>). 253 */ 254 public AxisState draw(Graphics2D g2, 255 double cursor, 256 Rectangle2D plotArea, 257 Rectangle2D dataArea, 258 RectangleEdge edge, 259 PlotRenderingInfo plotState) { 260 261 // if the axis is not visible, don't draw it... 262 if (!isVisible()) { 263 return new AxisState(cursor); 264 } 265 266 if (isAxisLineVisible()) { 267 drawAxisLine(g2, cursor, dataArea, edge); 268 } 269 270 // draw the category labels and axis label 271 AxisState state = new AxisState(cursor); 272 state = drawSubCategoryLabels(g2, plotArea, dataArea, edge, state, 273 plotState); 274 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state, 275 plotState); 276 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); 277 278 return state; 279 280 } 281 282 /** 283 * Draws the category labels and returns the updated axis state. 284 * 285 * @param g2 the graphics device (<code>null</code> not permitted). 286 * @param plotArea the plot area (<code>null</code> not permitted). 287 * @param dataArea the area inside the axes (<code>null</code> not 288 * permitted). 289 * @param edge the axis location (<code>null</code> not permitted). 290 * @param state the axis state (<code>null</code> not permitted). 291 * @param plotState collects information about the plot (<code>null</code> 292 * permitted). 293 * 294 * @return The updated axis state (never <code>null</code>). 295 */ 296 protected AxisState drawSubCategoryLabels(Graphics2D g2, 297 Rectangle2D plotArea, 298 Rectangle2D dataArea, 299 RectangleEdge edge, 300 AxisState state, 301 PlotRenderingInfo plotState) { 302 303 if (state == null) { 304 throw new IllegalArgumentException("Null 'state' argument."); 305 } 306 307 g2.setFont(this.subLabelFont); 308 g2.setPaint(this.subLabelPaint); 309 CategoryPlot plot = (CategoryPlot) getPlot(); 310 int categoryCount = 0; 311 CategoryDataset dataset = plot.getDataset(); 312 if (dataset != null) { 313 categoryCount = dataset.getColumnCount(); 314 } 315 316 double maxdim = getMaxDim(g2, edge); 317 for (int categoryIndex = 0; categoryIndex < categoryCount; 318 categoryIndex++) { 319 320 double x0 = 0.0; 321 double x1 = 0.0; 322 double y0 = 0.0; 323 double y1 = 0.0; 324 if (edge == RectangleEdge.TOP) { 325 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 326 edge); 327 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 328 edge); 329 y1 = state.getCursor(); 330 y0 = y1 - maxdim; 331 } 332 else if (edge == RectangleEdge.BOTTOM) { 333 x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 334 edge); 335 x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 336 edge); 337 y0 = state.getCursor(); 338 y1 = y0 + maxdim; 339 } 340 else if (edge == RectangleEdge.LEFT) { 341 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 342 edge); 343 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 344 edge); 345 x1 = state.getCursor(); 346 x0 = x1 - maxdim; 347 } 348 else if (edge == RectangleEdge.RIGHT) { 349 y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 350 edge); 351 y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 352 edge); 353 x0 = state.getCursor(); 354 x1 = x0 + maxdim; 355 } 356 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), 357 (y1 - y0)); 358 int subCategoryCount = this.subCategories.size(); 359 float width = (float) ((x1 - x0) / subCategoryCount); 360 float height = (float) ((y1 - y0) / subCategoryCount); 361 float xx = 0.0f; 362 float yy = 0.0f; 363 for (int i = 0; i < subCategoryCount; i++) { 364 if (RectangleEdge.isTopOrBottom(edge)) { 365 xx = (float) (x0 + (i + 0.5) * width); 366 yy = (float) area.getCenterY(); 367 } 368 else { 369 xx = (float) area.getCenterX(); 370 yy = (float) (y0 + (i + 0.5) * height); 371 } 372 String label = this.subCategories.get(i).toString(); 373 TextUtilities.drawRotatedString(label, g2, xx, yy, 374 TextAnchor.CENTER, 0.0, TextAnchor.CENTER); 375 } 376 } 377 378 if (edge.equals(RectangleEdge.TOP)) { 379 double h = maxdim; 380 state.cursorUp(h); 381 } 382 else if (edge.equals(RectangleEdge.BOTTOM)) { 383 double h = maxdim; 384 state.cursorDown(h); 385 } 386 else if (edge == RectangleEdge.LEFT) { 387 double w = maxdim; 388 state.cursorLeft(w); 389 } 390 else if (edge == RectangleEdge.RIGHT) { 391 double w = maxdim; 392 state.cursorRight(w); 393 } 394 return state; 395 } 396 397 /** 398 * Tests the axis for equality with an arbitrary object. 399 * 400 * @param obj the object (<code>null</code> permitted). 401 * 402 * @return A boolean. 403 */ 404 public boolean equals(Object obj) { 405 if (obj == this) { 406 return true; 407 } 408 if (obj instanceof SubCategoryAxis && super.equals(obj)) { 409 SubCategoryAxis axis = (SubCategoryAxis) obj; 410 if (!this.subCategories.equals(axis.subCategories)) { 411 return false; 412 } 413 if (!this.subLabelFont.equals(axis.subLabelFont)) { 414 return false; 415 } 416 if (!this.subLabelPaint.equals(axis.subLabelPaint)) { 417 return false; 418 } 419 return true; 420 } 421 return false; 422 } 423 424 /** 425 * Provides serialization support. 426 * 427 * @param stream the output stream. 428 * 429 * @throws IOException if there is an I/O error. 430 */ 431 private void writeObject(ObjectOutputStream stream) throws IOException { 432 stream.defaultWriteObject(); 433 SerialUtilities.writePaint(this.subLabelPaint, stream); 434 } 435 436 /** 437 * Provides serialization support. 438 * 439 * @param stream the input stream. 440 * 441 * @throws IOException if there is an I/O error. 442 * @throws ClassNotFoundException if there is a classpath problem. 443 */ 444 private void readObject(ObjectInputStream stream) 445 throws IOException, ClassNotFoundException { 446 stream.defaultReadObject(); 447 this.subLabelPaint = SerialUtilities.readPaint(stream); 448 } 449 450 }