001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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-2007, 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 * 047 */ 048 049 package org.jfree.chart.axis; 050 051 import java.awt.Color; 052 import java.awt.Font; 053 import java.awt.FontMetrics; 054 import java.awt.Graphics2D; 055 import java.awt.Paint; 056 import java.awt.geom.Rectangle2D; 057 import java.io.IOException; 058 import java.io.ObjectInputStream; 059 import java.io.ObjectOutputStream; 060 import java.io.Serializable; 061 import java.util.Iterator; 062 import java.util.List; 063 064 import org.jfree.chart.event.AxisChangeEvent; 065 import org.jfree.chart.plot.CategoryPlot; 066 import org.jfree.chart.plot.Plot; 067 import org.jfree.chart.plot.PlotRenderingInfo; 068 import org.jfree.data.category.CategoryDataset; 069 import org.jfree.io.SerialUtilities; 070 import org.jfree.text.TextUtilities; 071 import org.jfree.ui.RectangleEdge; 072 import org.jfree.ui.TextAnchor; 073 074 /** 075 * A specialised category axis that can display sub-categories. 076 */ 077 public class SubCategoryAxis extends CategoryAxis 078 implements Cloneable, Serializable { 079 080 /** For serialization. */ 081 private static final long serialVersionUID = -1279463299793228344L; 082 083 /** Storage for the sub-categories (these need to be set manually). */ 084 private List subCategories; 085 086 /** The font for the sub-category labels. */ 087 private Font subLabelFont = new Font("SansSerif", Font.PLAIN, 10); 088 089 /** The paint for the sub-category labels. */ 090 private transient Paint subLabelPaint = Color.black; 091 092 /** 093 * Creates a new axis. 094 * 095 * @param label the axis label. 096 */ 097 public SubCategoryAxis(String label) { 098 super(label); 099 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 }