1 /* =========================================================== 2 * JFreeChart : a free chart library for the Java(tm) platform 3 * =========================================================== 4 * 5 * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors. 6 * 7 * Project Info: http://www.jfree.org/jfreechart/index.html 8 * 9 * This library is free software; you can redistribute it and/or modify it 10 * under the terms of the GNU Lesser General Public License as published by 11 * the Free Software Foundation; either version 2.1 of the License, or 12 * (at your option) any later version. 13 * 14 * This library is distributed in the hope that it will be useful, but 15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 * License for more details. 18 * 19 * You should have received a copy of the GNU Lesser General Public 20 * License along with this library; if not, write to the Free Software 21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 * USA. 23 * 24 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 25 * Other names may be trademarks of their respective owners.] 26 * 27 * -------------------- 28 * SubCategoryAxis.java 29 * -------------------- 30 * (C) Copyright 2004-2013, by Object Refinery Limited. 31 * 32 * Original Author: David Gilbert; 33 * Contributor(s): Adriaan Joubert; 34 * 35 * Changes 36 * ------- 37 * 12-May-2004 : Version 1 (DG); 38 * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities 39 * --> TextUtilities (DG); 40 * 26-Apr-2005 : Removed logger (DG); 41 * ------------- JFREECHART 1.0.x --------------------------------------------- 42 * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan 43 * Joubert (1277726) (DG); 44 * 30-May-2007 : Added argument check and event notification to 45 * addSubCategory() (DG); 46 * 13-Nov-2008 : Fix NullPointerException when dataset is null - see bug 47 * report 2275695 (DG); 48 * 02-Jul-2013 : Use ParamChecks (DG); 49 * 01-Aug-2013 : Added attributedLabel override to support superscripts, 50 * subscripts and more (DG); 51 */ 52 53 package org.jfree.chart.axis; 54 55 import java.awt.Color; 56 import java.awt.Font; 57 import java.awt.FontMetrics; 58 import java.awt.Graphics2D; 59 import java.awt.Paint; 60 import java.awt.geom.Rectangle2D; 61 import java.io.IOException; 62 import java.io.ObjectInputStream; 63 import java.io.ObjectOutputStream; 64 import java.io.Serializable; 65 import java.util.Iterator; 66 import java.util.List; 67 68 import org.jfree.chart.event.AxisChangeEvent; 69 import org.jfree.chart.plot.CategoryPlot; 70 import org.jfree.chart.plot.Plot; 71 import org.jfree.chart.plot.PlotRenderingInfo; 72 import org.jfree.chart.util.ParamChecks; 73 import org.jfree.data.category.CategoryDataset; 74 import org.jfree.io.SerialUtilities; 75 import org.jfree.text.TextUtilities; 76 import org.jfree.ui.RectangleEdge; 77 import org.jfree.ui.TextAnchor; 78 79 /** 80 * A specialised category axis that can display sub-categories. 81 */ 82 public class SubCategoryAxis extends CategoryAxis 83 implements Cloneable, Serializable { 84 85 /** For serialization. */ 86 private static final long serialVersionUID = -1279463299793228344L; 87 88 /** Storage for the sub-categories (these need to be set manually). */ 89 private List subCategories; 90 91 /** The font for the sub-category labels. */ 92 private Font subLabelFont = new Font("SansSerif", Font.PLAIN, 10); 93 94 /** The paint for the sub-category labels. */ 95 private transient Paint subLabelPaint = Color.black; 96 97 /** 98 * Creates a new axis. 99 * 100 * @param label the axis label. 101 */ SubCategoryAxis(String label)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 */ addSubCategory(Comparable subCategory)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 */ getSubLabelFont()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 */ setSubLabelFont(Font font)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 */ getSubLabelPaint()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 */ setSubLabelPaint(Paint paint)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 reserveSpace(Graphics2D g2, Plot plot, Rectangle2D plotArea, RectangleEdge edge, AxisSpace space)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 */ getMaxDim(Graphics2D g2, RectangleEdge edge)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 draw(Graphics2D g2, double cursor, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState)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 */ drawSubCategoryLabels(Graphics2D g2, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, AxisState state, PlotRenderingInfo plotState)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 equals(Object obj)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 hashCode()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 */ writeObject(ObjectOutputStream stream)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 */ readObject(ObjectInputStream stream)448 private void readObject(ObjectInputStream stream) 449 throws IOException, ClassNotFoundException { 450 stream.defaultReadObject(); 451 this.subLabelPaint = SerialUtilities.readPaint(stream); 452 } 453 454 } 455