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