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