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