001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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-2007, 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 060package org.jfree.chart.renderer.category; 061 062import java.awt.BasicStroke; 063import java.awt.Color; 064import java.awt.Component; 065import java.awt.Graphics; 066import java.awt.Graphics2D; 067import java.awt.Paint; 068import java.awt.Shape; 069import java.awt.Stroke; 070import java.awt.geom.AffineTransform; 071import java.awt.geom.Arc2D; 072import java.awt.geom.GeneralPath; 073import java.awt.geom.Line2D; 074import java.awt.geom.Rectangle2D; 075import java.io.IOException; 076import java.io.ObjectInputStream; 077import java.io.ObjectOutputStream; 078 079import javax.swing.Icon; 080 081import org.jfree.chart.axis.CategoryAxis; 082import org.jfree.chart.axis.ValueAxis; 083import org.jfree.chart.entity.EntityCollection; 084import org.jfree.chart.event.RendererChangeEvent; 085import org.jfree.chart.plot.CategoryPlot; 086import org.jfree.chart.plot.PlotOrientation; 087import org.jfree.data.category.CategoryDataset; 088import org.jfree.io.SerialUtilities; 089import 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 */ 098public 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 this.notifyListeners(new RendererChangeEvent(this)); 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 notifyListeners(new RendererChangeEvent(this)); 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 notifyListeners(new RendererChangeEvent(this)); 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. 242 * 243 * @param icon the icon. 244 * 245 * @see #getObjectIcon() 246 */ 247 public void setObjectIcon(Icon icon) { 248 if (icon == null) { 249 throw new IllegalArgumentException("Null 'icon' argument."); 250 } 251 this.objectIcon = icon; 252 notifyListeners(new RendererChangeEvent(this)); 253 } 254 255 /** 256 * Returns the icon displayed for the maximum value data item within each 257 * category. 258 * 259 * @return The icon (never <code>null</code>). 260 * 261 * @see #setMaxIcon(Icon) 262 */ 263 public Icon getMaxIcon() { 264 return this.maxIcon; 265 } 266 267 /** 268 * Sets the icon displayed for the maximum value data item within each 269 * category and sends a {@link RendererChangeEvent} to all registered 270 * listeners. 271 * 272 * @param icon the icon (<code>null</code> not permitted). 273 * 274 * @see #getMaxIcon() 275 */ 276 public void setMaxIcon(Icon icon) { 277 if (icon == null) { 278 throw new IllegalArgumentException("Null 'icon' argument."); 279 } 280 this.maxIcon = icon; 281 notifyListeners(new RendererChangeEvent(this)); 282 } 283 284 /** 285 * Returns the icon displayed for the minimum value data item within each 286 * category. 287 * 288 * @return The icon (never <code>null</code>). 289 * 290 * @see #setMinIcon(Icon) 291 */ 292 public Icon getMinIcon() { 293 return this.minIcon; 294 } 295 296 /** 297 * Sets the icon displayed for the minimum value data item within each 298 * category and sends a {@link RendererChangeEvent} to all registered 299 * listeners. 300 * 301 * @param icon the icon (<code>null</code> not permitted). 302 * 303 * @see #getMinIcon() 304 */ 305 public void setMinIcon(Icon icon) { 306 if (icon == null) { 307 throw new IllegalArgumentException("Null 'icon' argument."); 308 } 309 this.minIcon = icon; 310 notifyListeners(new RendererChangeEvent(this)); 311 } 312 313 /** 314 * Draw a single data item. 315 * 316 * @param g2 the graphics device. 317 * @param state the renderer state. 318 * @param dataArea the area in which the data is drawn. 319 * @param plot the plot. 320 * @param domainAxis the domain axis. 321 * @param rangeAxis the range axis. 322 * @param dataset the dataset. 323 * @param row the row index (zero-based). 324 * @param column the column index (zero-based). 325 * @param pass the pass index. 326 */ 327 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 328 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 329 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 330 int pass) { 331 332 // first check the number we are plotting... 333 Number value = dataset.getValue(row, column); 334 if (value != null) { 335 // current data point... 336 double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 337 dataArea, plot.getDomainAxisEdge()); 338 double y1 = rangeAxis.valueToJava2D(value.doubleValue(), dataArea, 339 plot.getRangeAxisEdge()); 340 g2.setPaint(getItemPaint(row, column)); 341 g2.setStroke(getItemStroke(row, column)); 342 Shape shape = null; 343 shape = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0); 344 345 PlotOrientation orient = plot.getOrientation(); 346 if (orient == PlotOrientation.VERTICAL) { 347 this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1); 348 } 349 else { 350 this.objectIcon.paintIcon(null, g2, (int) y1, (int) x1); 351 } 352 353 if (this.lastCategory == column) { 354 if (this.min > value.doubleValue()) { 355 this.min = value.doubleValue(); 356 } 357 if (this.max < value.doubleValue()) { 358 this.max = value.doubleValue(); 359 } 360 361 // last series, so we are ready to draw the min and max 362 if (dataset.getRowCount() - 1 == row) { 363 g2.setPaint(this.groupPaint); 364 g2.setStroke(this.groupStroke); 365 double minY = rangeAxis.valueToJava2D(this.min, dataArea, 366 plot.getRangeAxisEdge()); 367 double maxY = rangeAxis.valueToJava2D(this.max, dataArea, 368 plot.getRangeAxisEdge()); 369 370 if (orient == PlotOrientation.VERTICAL) { 371 g2.draw(new Line2D.Double(x1, minY, x1, maxY)); 372 this.minIcon.paintIcon(null, g2, (int) x1, (int) minY); 373 this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY); 374 } 375 else { 376 g2.draw(new Line2D.Double(minY, x1, maxY, x1)); 377 this.minIcon.paintIcon(null, g2, (int) minY, (int) x1); 378 this.maxIcon.paintIcon(null, g2, (int) maxY, (int) x1); 379 } 380 } 381 } 382 else { // reset the min and max 383 this.lastCategory = column; 384 this.min = value.doubleValue(); 385 this.max = value.doubleValue(); 386 } 387 388 // connect to the previous point 389 if (this.plotLines) { 390 if (column != 0) { 391 Number previousValue = dataset.getValue(row, column - 1); 392 if (previousValue != null) { 393 // previous data point... 394 double previous = previousValue.doubleValue(); 395 double x0 = domainAxis.getCategoryMiddle(column - 1, 396 getColumnCount(), dataArea, 397 plot.getDomainAxisEdge()); 398 double y0 = rangeAxis.valueToJava2D(previous, dataArea, 399 plot.getRangeAxisEdge()); 400 g2.setPaint(getItemPaint(row, column)); 401 g2.setStroke(getItemStroke(row, column)); 402 Line2D line; 403 if (orient == PlotOrientation.VERTICAL) { 404 line = new Line2D.Double(x0, y0, x1, y1); 405 } 406 else { 407 line = new Line2D.Double(y0, x0, y1, x1); 408 } 409 g2.draw(line); 410 } 411 } 412 } 413 414 // add an item entity, if this information is being collected 415 EntityCollection entities = state.getEntityCollection(); 416 if (entities != null && shape != null) { 417 addItemEntity(entities, dataset, row, column, shape); 418 } 419 } 420 } 421 422 /** 423 * Tests this instance for equality with an arbitrary object. The icon 424 * fields are NOT included in the test, so this implementation is a little 425 * weak. 426 * 427 * @param obj the object (<code>null</code> permitted). 428 * 429 * @return A boolean. 430 * 431 * @since 1.0.7 432 */ 433 public boolean equals(Object obj) { 434 if (obj == this) { 435 return true; 436 } 437 if (!(obj instanceof MinMaxCategoryRenderer)) { 438 return false; 439 } 440 MinMaxCategoryRenderer that = (MinMaxCategoryRenderer) obj; 441 if (this.plotLines != that.plotLines) { 442 return false; 443 } 444 if (!PaintUtilities.equal(this.groupPaint, that.groupPaint)) { 445 return false; 446 } 447 if (!this.groupStroke.equals(that.groupStroke)) { 448 return false; 449 } 450 return super.equals(obj); 451 } 452 453 /** 454 * Returns an icon. 455 * 456 * @param shape the shape. 457 * @param fillPaint the fill paint. 458 * @param outlinePaint the outline paint. 459 * 460 * @return The icon. 461 */ 462 private Icon getIcon(Shape shape, final Paint fillPaint, 463 final Paint outlinePaint) { 464 465 final int width = shape.getBounds().width; 466 final int height = shape.getBounds().height; 467 final GeneralPath path = new GeneralPath(shape); 468 return new Icon() { 469 public void paintIcon(Component c, Graphics g, int x, int y) { 470 Graphics2D g2 = (Graphics2D) g; 471 path.transform(AffineTransform.getTranslateInstance(x, y)); 472 if (fillPaint != null) { 473 g2.setPaint(fillPaint); 474 g2.fill(path); 475 } 476 if (outlinePaint != null) { 477 g2.setPaint(outlinePaint); 478 g2.draw(path); 479 } 480 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 481 } 482 483 public int getIconWidth() { 484 return width; 485 } 486 487 public int getIconHeight() { 488 return height; 489 } 490 491 }; 492 } 493 494 /** 495 * Returns an icon from a shape. 496 * 497 * @param shape the shape. 498 * @param fill the fill flag. 499 * @param outline the outline flag. 500 * 501 * @return The icon. 502 */ 503 private Icon getIcon(Shape shape, final boolean fill, 504 final boolean outline) { 505 final int width = shape.getBounds().width; 506 final int height = shape.getBounds().height; 507 final GeneralPath path = new GeneralPath(shape); 508 return new Icon() { 509 public void paintIcon(Component c, Graphics g, int x, int y) { 510 Graphics2D g2 = (Graphics2D) g; 511 path.transform(AffineTransform.getTranslateInstance(x, y)); 512 if (fill) { 513 g2.fill(path); 514 } 515 if (outline) { 516 g2.draw(path); 517 } 518 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 519 } 520 521 public int getIconWidth() { 522 return width; 523 } 524 525 public int getIconHeight() { 526 return height; 527 } 528 }; 529 } 530 531 /** 532 * Provides serialization support. 533 * 534 * @param stream the output stream. 535 * 536 * @throws IOException if there is an I/O error. 537 */ 538 private void writeObject(ObjectOutputStream stream) throws IOException { 539 stream.defaultWriteObject(); 540 SerialUtilities.writeStroke(this.groupStroke, stream); 541 SerialUtilities.writePaint(this.groupPaint, stream); 542 } 543 544 /** 545 * Provides serialization support. 546 * 547 * @param stream the input stream. 548 * 549 * @throws IOException if there is an I/O error. 550 * @throws ClassNotFoundException if there is a classpath problem. 551 */ 552 private void readObject(ObjectInputStream stream) 553 throws IOException, ClassNotFoundException { 554 stream.defaultReadObject(); 555 this.groupStroke = SerialUtilities.readStroke(stream); 556 this.groupPaint = SerialUtilities.readPaint(stream); 557 558 this.minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 559 Arc2D.OPEN), null, Color.black); 560 this.maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 561 Arc2D.OPEN), null, Color.black); 562 this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true); 563 } 564 565}