001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2006, 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-2006, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Adriaan Joubert; 034 * 035 * $Id: SubCategoryAxis.java,v 1.6.2.2 2006/08/18 14:48:31 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 * ------------- JFREECHART 1.0.0 --------------------------------------------- 044 * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan 045 * Joubert (1277726) (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. 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 }