001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * MinMaxCategoryRenderer.java 029 * --------------------------- 030 * (C) Copyright 2002-2008, by Object Refinery Limited. 031 * 032 * Original Author: Tomer Peretz; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Christian W. Zuckschwerdt; 035 * Nicolas Brodu (for Astrium and EADS Corporate Research 036 * Center); 037 * 038 * Changes: 039 * -------- 040 * 29-May-2002 : Version 1 (TP); 041 * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG); 042 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 043 * CategoryToolTipGenerator interface (DG); 044 * 05-Nov-2002 : Base dataset is now TableDataset not CategoryDataset (DG); 045 * 17-Jan-2003 : Moved plot classes to a separate package (DG); 046 * 10-Apr-2003 : Changed CategoryDataset to KeyedValues2DDataset in drawItem() 047 * method (DG); 048 * 30-Jul-2003 : Modified entity constructor (CZ); 049 * 08-Sep-2003 : Implemented Serializable (NB); 050 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG); 051 * 05-Nov-2004 : Modified drawItem() signature (DG); 052 * 17-Nov-2005 : Added change events and argument checks (DG); 053 * ------------- JFREECHART 1.0.x --------------------------------------------- 054 * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG); 055 * 09-Mar-2007 : Fixed problem with horizontal rendering (DG); 056 * 28-Sep-2007 : Added equals() method override (DG); 057 * 058 */ 059 060 package org.jfree.chart.renderer.category; 061 062 import java.awt.BasicStroke; 063 import java.awt.Color; 064 import java.awt.Component; 065 import java.awt.Graphics; 066 import java.awt.Graphics2D; 067 import java.awt.Paint; 068 import java.awt.Shape; 069 import java.awt.Stroke; 070 import java.awt.geom.AffineTransform; 071 import java.awt.geom.Arc2D; 072 import java.awt.geom.GeneralPath; 073 import java.awt.geom.Line2D; 074 import java.awt.geom.Rectangle2D; 075 import java.io.IOException; 076 import java.io.ObjectInputStream; 077 import java.io.ObjectOutputStream; 078 079 import javax.swing.Icon; 080 081 import org.jfree.chart.axis.CategoryAxis; 082 import org.jfree.chart.axis.ValueAxis; 083 import org.jfree.chart.entity.EntityCollection; 084 import org.jfree.chart.event.RendererChangeEvent; 085 import org.jfree.chart.plot.CategoryPlot; 086 import org.jfree.chart.plot.PlotOrientation; 087 import org.jfree.data.category.CategoryDataset; 088 import org.jfree.io.SerialUtilities; 089 import org.jfree.util.PaintUtilities; 090 091 /** 092 * Renderer for drawing min max plot. This renderer draws all the series under 093 * the same category in the same x position using <code>objectIcon</code> and 094 * a line from the maximum value to the minimum value. 095 * <p> 096 * For use with the {@link org.jfree.chart.plot.CategoryPlot} class. 097 */ 098 public class MinMaxCategoryRenderer extends AbstractCategoryItemRenderer { 099 100 /** For serialization. */ 101 private static final long serialVersionUID = 2935615937671064911L; 102 103 /** A flag indicating whether or not lines are drawn between XY points. */ 104 private boolean plotLines = false; 105 106 /** 107 * The paint of the line between the minimum value and the maximum value. 108 */ 109 private transient Paint groupPaint = Color.black; 110 111 /** 112 * The stroke of the line between the minimum value and the maximum value. 113 */ 114 private transient Stroke groupStroke = new BasicStroke(1.0f); 115 116 /** The icon used to indicate the minimum value.*/ 117 private transient Icon minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 118 360, Arc2D.OPEN), null, Color.black); 119 120 /** The icon used to indicate the maximum value.*/ 121 private transient Icon maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 122 360, Arc2D.OPEN), null, Color.black); 123 124 /** The icon used to indicate the values.*/ 125 private transient Icon objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), 126 false, true); 127 128 /** The last category. */ 129 private int lastCategory = -1; 130 131 /** The minimum. */ 132 private double min; 133 134 /** The maximum. */ 135 private double max; 136 137 /** 138 * Default constructor. 139 */ 140 public MinMaxCategoryRenderer() { 141 super(); 142 } 143 144 /** 145 * Gets whether or not lines are drawn between category points. 146 * 147 * @return boolean true if line will be drawn between sequenced categories, 148 * otherwise false. 149 * 150 * @see #setDrawLines(boolean) 151 */ 152 public boolean isDrawLines() { 153 return this.plotLines; 154 } 155 156 /** 157 * Sets the flag that controls whether or not lines are drawn to connect 158 * the items within a series and sends a {@link RendererChangeEvent} to 159 * all registered listeners. 160 * 161 * @param draw the new value of the flag. 162 * 163 * @see #isDrawLines() 164 */ 165 public void setDrawLines(boolean draw) { 166 if (this.plotLines != draw) { 167 this.plotLines = draw; 168 fireChangeEvent(); 169 } 170 171 } 172 173 /** 174 * Returns the paint used to draw the line between the minimum and maximum 175 * value items in each category. 176 * 177 * @return The paint (never <code>null</code>). 178 * 179 * @see #setGroupPaint(Paint) 180 */ 181 public Paint getGroupPaint() { 182 return this.groupPaint; 183 } 184 185 /** 186 * Sets the paint used to draw the line between the minimum and maximum 187 * value items in each category and sends a {@link RendererChangeEvent} to 188 * all registered listeners. 189 * 190 * @param paint the paint (<code>null</code> not permitted). 191 * 192 * @see #getGroupPaint() 193 */ 194 public void setGroupPaint(Paint paint) { 195 if (paint == null) { 196 throw new IllegalArgumentException("Null 'paint' argument."); 197 } 198 this.groupPaint = paint; 199 fireChangeEvent(); 200 } 201 202 /** 203 * Returns the stroke used to draw the line between the minimum and maximum 204 * value items in each category. 205 * 206 * @return The stroke (never <code>null</code>). 207 * 208 * @see #setGroupStroke(Stroke) 209 */ 210 public Stroke getGroupStroke() { 211 return this.groupStroke; 212 } 213 214 /** 215 * Sets the stroke of the line between the minimum value and the maximum 216 * value and sends a {@link RendererChangeEvent} to all registered 217 * listeners. 218 * 219 * @param stroke the new stroke (<code>null</code> not permitted). 220 */ 221 public void setGroupStroke(Stroke stroke) { 222 if (stroke == null) { 223 throw new IllegalArgumentException("Null 'stroke' argument."); 224 } 225 this.groupStroke = stroke; 226 fireChangeEvent(); 227 } 228 229 /** 230 * Returns the icon drawn for each data item. 231 * 232 * @return The icon (never <code>null</code>). 233 * 234 * @see #setObjectIcon(Icon) 235 */ 236 public Icon getObjectIcon() { 237 return this.objectIcon; 238 } 239 240 /** 241 * Sets the icon drawn for each data item and sends a 242 * {@link RendererChangeEvent} to all registered listeners. 243 * 244 * @param icon the icon. 245 * 246 * @see #getObjectIcon() 247 */ 248 public void setObjectIcon(Icon icon) { 249 if (icon == null) { 250 throw new IllegalArgumentException("Null 'icon' argument."); 251 } 252 this.objectIcon = icon; 253 fireChangeEvent(); 254 } 255 256 /** 257 * Returns the icon displayed for the maximum value data item within each 258 * category. 259 * 260 * @return The icon (never <code>null</code>). 261 * 262 * @see #setMaxIcon(Icon) 263 */ 264 public Icon getMaxIcon() { 265 return this.maxIcon; 266 } 267 268 /** 269 * Sets the icon displayed for the maximum value data item within each 270 * category and sends a {@link RendererChangeEvent} to all registered 271 * listeners. 272 * 273 * @param icon the icon (<code>null</code> not permitted). 274 * 275 * @see #getMaxIcon() 276 */ 277 public void setMaxIcon(Icon icon) { 278 if (icon == null) { 279 throw new IllegalArgumentException("Null 'icon' argument."); 280 } 281 this.maxIcon = icon; 282 fireChangeEvent(); 283 } 284 285 /** 286 * Returns the icon displayed for the minimum value data item within each 287 * category. 288 * 289 * @return The icon (never <code>null</code>). 290 * 291 * @see #setMinIcon(Icon) 292 */ 293 public Icon getMinIcon() { 294 return this.minIcon; 295 } 296 297 /** 298 * Sets the icon displayed for the minimum value data item within each 299 * category and sends a {@link RendererChangeEvent} to all registered 300 * listeners. 301 * 302 * @param icon the icon (<code>null</code> not permitted). 303 * 304 * @see #getMinIcon() 305 */ 306 public void setMinIcon(Icon icon) { 307 if (icon == null) { 308 throw new IllegalArgumentException("Null 'icon' argument."); 309 } 310 this.minIcon = icon; 311 fireChangeEvent(); 312 } 313 314 /** 315 * Draw a single data item. 316 * 317 * @param g2 the graphics device. 318 * @param state the renderer state. 319 * @param dataArea the area in which the data is drawn. 320 * @param plot the plot. 321 * @param domainAxis the domain axis. 322 * @param rangeAxis the range axis. 323 * @param dataset the dataset. 324 * @param row the row index (zero-based). 325 * @param column the column index (zero-based). 326 * @param pass the pass index. 327 */ 328 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 329 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 330 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 331 int pass) { 332 333 // first check the number we are plotting... 334 Number value = dataset.getValue(row, column); 335 if (value != null) { 336 // current data point... 337 double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 338 dataArea, plot.getDomainAxisEdge()); 339 double y1 = rangeAxis.valueToJava2D(value.doubleValue(), dataArea, 340 plot.getRangeAxisEdge()); 341 g2.setPaint(getItemPaint(row, column)); 342 g2.setStroke(getItemStroke(row, column)); 343 Shape shape = null; 344 shape = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0); 345 346 PlotOrientation orient = plot.getOrientation(); 347 if (orient == PlotOrientation.VERTICAL) { 348 this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1); 349 } 350 else { 351 this.objectIcon.paintIcon(null, g2, (int) y1, (int) x1); 352 } 353 354 if (this.lastCategory == column) { 355 if (this.min > value.doubleValue()) { 356 this.min = value.doubleValue(); 357 } 358 if (this.max < value.doubleValue()) { 359 this.max = value.doubleValue(); 360 } 361 362 // last series, so we are ready to draw the min and max 363 if (dataset.getRowCount() - 1 == row) { 364 g2.setPaint(this.groupPaint); 365 g2.setStroke(this.groupStroke); 366 double minY = rangeAxis.valueToJava2D(this.min, dataArea, 367 plot.getRangeAxisEdge()); 368 double maxY = rangeAxis.valueToJava2D(this.max, dataArea, 369 plot.getRangeAxisEdge()); 370 371 if (orient == PlotOrientation.VERTICAL) { 372 g2.draw(new Line2D.Double(x1, minY, x1, maxY)); 373 this.minIcon.paintIcon(null, g2, (int) x1, (int) minY); 374 this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY); 375 } 376 else { 377 g2.draw(new Line2D.Double(minY, x1, maxY, x1)); 378 this.minIcon.paintIcon(null, g2, (int) minY, (int) x1); 379 this.maxIcon.paintIcon(null, g2, (int) maxY, (int) x1); 380 } 381 } 382 } 383 else { // reset the min and max 384 this.lastCategory = column; 385 this.min = value.doubleValue(); 386 this.max = value.doubleValue(); 387 } 388 389 // connect to the previous point 390 if (this.plotLines) { 391 if (column != 0) { 392 Number previousValue = dataset.getValue(row, column - 1); 393 if (previousValue != null) { 394 // previous data point... 395 double previous = previousValue.doubleValue(); 396 double x0 = domainAxis.getCategoryMiddle(column - 1, 397 getColumnCount(), dataArea, 398 plot.getDomainAxisEdge()); 399 double y0 = rangeAxis.valueToJava2D(previous, dataArea, 400 plot.getRangeAxisEdge()); 401 g2.setPaint(getItemPaint(row, column)); 402 g2.setStroke(getItemStroke(row, column)); 403 Line2D line; 404 if (orient == PlotOrientation.VERTICAL) { 405 line = new Line2D.Double(x0, y0, x1, y1); 406 } 407 else { 408 line = new Line2D.Double(y0, x0, y1, x1); 409 } 410 g2.draw(line); 411 } 412 } 413 } 414 415 // add an item entity, if this information is being collected 416 EntityCollection entities = state.getEntityCollection(); 417 if (entities != null && shape != null) { 418 addItemEntity(entities, dataset, row, column, shape); 419 } 420 } 421 } 422 423 /** 424 * Tests this instance for equality with an arbitrary object. The icon 425 * fields are NOT included in the test, so this implementation is a little 426 * weak. 427 * 428 * @param obj the object (<code>null</code> permitted). 429 * 430 * @return A boolean. 431 * 432 * @since 1.0.7 433 */ 434 public boolean equals(Object obj) { 435 if (obj == this) { 436 return true; 437 } 438 if (!(obj instanceof MinMaxCategoryRenderer)) { 439 return false; 440 } 441 MinMaxCategoryRenderer that = (MinMaxCategoryRenderer) obj; 442 if (this.plotLines != that.plotLines) { 443 return false; 444 } 445 if (!PaintUtilities.equal(this.groupPaint, that.groupPaint)) { 446 return false; 447 } 448 if (!this.groupStroke.equals(that.groupStroke)) { 449 return false; 450 } 451 return super.equals(obj); 452 } 453 454 /** 455 * Returns an icon. 456 * 457 * @param shape the shape. 458 * @param fillPaint the fill paint. 459 * @param outlinePaint the outline paint. 460 * 461 * @return The icon. 462 */ 463 private Icon getIcon(Shape shape, final Paint fillPaint, 464 final Paint outlinePaint) { 465 466 final int width = shape.getBounds().width; 467 final int height = shape.getBounds().height; 468 final GeneralPath path = new GeneralPath(shape); 469 return new Icon() { 470 public void paintIcon(Component c, Graphics g, int x, int y) { 471 Graphics2D g2 = (Graphics2D) g; 472 path.transform(AffineTransform.getTranslateInstance(x, y)); 473 if (fillPaint != null) { 474 g2.setPaint(fillPaint); 475 g2.fill(path); 476 } 477 if (outlinePaint != null) { 478 g2.setPaint(outlinePaint); 479 g2.draw(path); 480 } 481 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 482 } 483 484 public int getIconWidth() { 485 return width; 486 } 487 488 public int getIconHeight() { 489 return height; 490 } 491 492 }; 493 } 494 495 /** 496 * Returns an icon from a shape. 497 * 498 * @param shape the shape. 499 * @param fill the fill flag. 500 * @param outline the outline flag. 501 * 502 * @return The icon. 503 */ 504 private Icon getIcon(Shape shape, final boolean fill, 505 final boolean outline) { 506 final int width = shape.getBounds().width; 507 final int height = shape.getBounds().height; 508 final GeneralPath path = new GeneralPath(shape); 509 return new Icon() { 510 public void paintIcon(Component c, Graphics g, int x, int y) { 511 Graphics2D g2 = (Graphics2D) g; 512 path.transform(AffineTransform.getTranslateInstance(x, y)); 513 if (fill) { 514 g2.fill(path); 515 } 516 if (outline) { 517 g2.draw(path); 518 } 519 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 520 } 521 522 public int getIconWidth() { 523 return width; 524 } 525 526 public int getIconHeight() { 527 return height; 528 } 529 }; 530 } 531 532 /** 533 * Provides serialization support. 534 * 535 * @param stream the output stream. 536 * 537 * @throws IOException if there is an I/O error. 538 */ 539 private void writeObject(ObjectOutputStream stream) throws IOException { 540 stream.defaultWriteObject(); 541 SerialUtilities.writeStroke(this.groupStroke, stream); 542 SerialUtilities.writePaint(this.groupPaint, stream); 543 } 544 545 /** 546 * Provides serialization support. 547 * 548 * @param stream the input stream. 549 * 550 * @throws IOException if there is an I/O error. 551 * @throws ClassNotFoundException if there is a classpath problem. 552 */ 553 private void readObject(ObjectInputStream stream) 554 throws IOException, ClassNotFoundException { 555 stream.defaultReadObject(); 556 this.groupStroke = SerialUtilities.readStroke(stream); 557 this.groupPaint = SerialUtilities.readPaint(stream); 558 559 this.minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 560 Arc2D.OPEN), null, Color.black); 561 this.maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 562 Arc2D.OPEN), null, Color.black); 563 this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true); 564 } 565 566 }