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