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. For use with the 095 * {@link CategoryPlot} class. The example shown here is generated by 096 * the <code>MinMaxCategoryPlotDemo1.java</code> program included in the 097 * JFreeChart Demo Collection: 098 * <br><br> 099 * <img src="../../../../../images/MinMaxCategoryRendererSample.png" 100 * alt="MinMaxCategoryRendererSample.png" /> 101 */ 102 public class MinMaxCategoryRenderer extends AbstractCategoryItemRenderer { 103 104 /** For serialization. */ 105 private static final long serialVersionUID = 2935615937671064911L; 106 107 /** A flag indicating whether or not lines are drawn between XY points. */ 108 private boolean plotLines = false; 109 110 /** 111 * The paint of the line between the minimum value and the maximum value. 112 */ 113 private transient Paint groupPaint = Color.black; 114 115 /** 116 * The stroke of the line between the minimum value and the maximum value. 117 */ 118 private transient Stroke groupStroke = new BasicStroke(1.0f); 119 120 /** The icon used to indicate the minimum value.*/ 121 private transient Icon minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 122 360, Arc2D.OPEN), null, Color.black); 123 124 /** The icon used to indicate the maximum value.*/ 125 private transient Icon maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 126 360, Arc2D.OPEN), null, Color.black); 127 128 /** The icon used to indicate the values.*/ 129 private transient Icon objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), 130 false, true); 131 132 /** The last category. */ 133 private int lastCategory = -1; 134 135 /** The minimum. */ 136 private double min; 137 138 /** The maximum. */ 139 private double max; 140 141 /** 142 * Default constructor. 143 */ 144 public MinMaxCategoryRenderer() { 145 super(); 146 } 147 148 /** 149 * Gets whether or not lines are drawn between category points. 150 * 151 * @return boolean true if line will be drawn between sequenced categories, 152 * otherwise false. 153 * 154 * @see #setDrawLines(boolean) 155 */ 156 public boolean isDrawLines() { 157 return this.plotLines; 158 } 159 160 /** 161 * Sets the flag that controls whether or not lines are drawn to connect 162 * the items within a series and sends a {@link RendererChangeEvent} to 163 * all registered listeners. 164 * 165 * @param draw the new value of the flag. 166 * 167 * @see #isDrawLines() 168 */ 169 public void setDrawLines(boolean draw) { 170 if (this.plotLines != draw) { 171 this.plotLines = draw; 172 fireChangeEvent(); 173 } 174 175 } 176 177 /** 178 * Returns the paint used to draw the line between the minimum and maximum 179 * value items in each category. 180 * 181 * @return The paint (never <code>null</code>). 182 * 183 * @see #setGroupPaint(Paint) 184 */ 185 public Paint getGroupPaint() { 186 return this.groupPaint; 187 } 188 189 /** 190 * Sets the paint used to draw the line between the minimum and maximum 191 * value items in each category and sends a {@link RendererChangeEvent} to 192 * all registered listeners. 193 * 194 * @param paint the paint (<code>null</code> not permitted). 195 * 196 * @see #getGroupPaint() 197 */ 198 public void setGroupPaint(Paint paint) { 199 if (paint == null) { 200 throw new IllegalArgumentException("Null 'paint' argument."); 201 } 202 this.groupPaint = paint; 203 fireChangeEvent(); 204 } 205 206 /** 207 * Returns the stroke used to draw the line between the minimum and maximum 208 * value items in each category. 209 * 210 * @return The stroke (never <code>null</code>). 211 * 212 * @see #setGroupStroke(Stroke) 213 */ 214 public Stroke getGroupStroke() { 215 return this.groupStroke; 216 } 217 218 /** 219 * Sets the stroke of the line between the minimum value and the maximum 220 * value and sends a {@link RendererChangeEvent} to all registered 221 * listeners. 222 * 223 * @param stroke the new stroke (<code>null</code> not permitted). 224 */ 225 public void setGroupStroke(Stroke stroke) { 226 if (stroke == null) { 227 throw new IllegalArgumentException("Null 'stroke' argument."); 228 } 229 this.groupStroke = stroke; 230 fireChangeEvent(); 231 } 232 233 /** 234 * Returns the icon drawn for each data item. 235 * 236 * @return The icon (never <code>null</code>). 237 * 238 * @see #setObjectIcon(Icon) 239 */ 240 public Icon getObjectIcon() { 241 return this.objectIcon; 242 } 243 244 /** 245 * Sets the icon drawn for each data item and sends a 246 * {@link RendererChangeEvent} to all registered listeners. 247 * 248 * @param icon the icon. 249 * 250 * @see #getObjectIcon() 251 */ 252 public void setObjectIcon(Icon icon) { 253 if (icon == null) { 254 throw new IllegalArgumentException("Null 'icon' argument."); 255 } 256 this.objectIcon = icon; 257 fireChangeEvent(); 258 } 259 260 /** 261 * Returns the icon displayed for the maximum value data item within each 262 * category. 263 * 264 * @return The icon (never <code>null</code>). 265 * 266 * @see #setMaxIcon(Icon) 267 */ 268 public Icon getMaxIcon() { 269 return this.maxIcon; 270 } 271 272 /** 273 * Sets the icon displayed for the maximum value data item within each 274 * category and sends a {@link RendererChangeEvent} to all registered 275 * listeners. 276 * 277 * @param icon the icon (<code>null</code> not permitted). 278 * 279 * @see #getMaxIcon() 280 */ 281 public void setMaxIcon(Icon icon) { 282 if (icon == null) { 283 throw new IllegalArgumentException("Null 'icon' argument."); 284 } 285 this.maxIcon = icon; 286 fireChangeEvent(); 287 } 288 289 /** 290 * Returns the icon displayed for the minimum value data item within each 291 * category. 292 * 293 * @return The icon (never <code>null</code>). 294 * 295 * @see #setMinIcon(Icon) 296 */ 297 public Icon getMinIcon() { 298 return this.minIcon; 299 } 300 301 /** 302 * Sets the icon displayed for the minimum value data item within each 303 * category and sends a {@link RendererChangeEvent} to all registered 304 * listeners. 305 * 306 * @param icon the icon (<code>null</code> not permitted). 307 * 308 * @see #getMinIcon() 309 */ 310 public void setMinIcon(Icon icon) { 311 if (icon == null) { 312 throw new IllegalArgumentException("Null 'icon' argument."); 313 } 314 this.minIcon = icon; 315 fireChangeEvent(); 316 } 317 318 /** 319 * Draw a single data item. 320 * 321 * @param g2 the graphics device. 322 * @param state the renderer state. 323 * @param dataArea the area in which the data is drawn. 324 * @param plot the plot. 325 * @param domainAxis the domain axis. 326 * @param rangeAxis the range axis. 327 * @param dataset the dataset. 328 * @param row the row index (zero-based). 329 * @param column the column index (zero-based). 330 * @param pass the pass index. 331 */ 332 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 333 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 334 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 335 int pass) { 336 337 // first check the number we are plotting... 338 Number value = dataset.getValue(row, column); 339 if (value != null) { 340 // current data point... 341 double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 342 dataArea, plot.getDomainAxisEdge()); 343 double y1 = rangeAxis.valueToJava2D(value.doubleValue(), dataArea, 344 plot.getRangeAxisEdge()); 345 g2.setPaint(getItemPaint(row, column)); 346 g2.setStroke(getItemStroke(row, column)); 347 Shape shape = null; 348 shape = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0); 349 350 PlotOrientation orient = plot.getOrientation(); 351 if (orient == PlotOrientation.VERTICAL) { 352 this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1); 353 } 354 else { 355 this.objectIcon.paintIcon(null, g2, (int) y1, (int) x1); 356 } 357 358 if (this.lastCategory == column) { 359 if (this.min > value.doubleValue()) { 360 this.min = value.doubleValue(); 361 } 362 if (this.max < value.doubleValue()) { 363 this.max = value.doubleValue(); 364 } 365 366 // last series, so we are ready to draw the min and max 367 if (dataset.getRowCount() - 1 == row) { 368 g2.setPaint(this.groupPaint); 369 g2.setStroke(this.groupStroke); 370 double minY = rangeAxis.valueToJava2D(this.min, dataArea, 371 plot.getRangeAxisEdge()); 372 double maxY = rangeAxis.valueToJava2D(this.max, dataArea, 373 plot.getRangeAxisEdge()); 374 375 if (orient == PlotOrientation.VERTICAL) { 376 g2.draw(new Line2D.Double(x1, minY, x1, maxY)); 377 this.minIcon.paintIcon(null, g2, (int) x1, (int) minY); 378 this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY); 379 } 380 else { 381 g2.draw(new Line2D.Double(minY, x1, maxY, x1)); 382 this.minIcon.paintIcon(null, g2, (int) minY, (int) x1); 383 this.maxIcon.paintIcon(null, g2, (int) maxY, (int) x1); 384 } 385 } 386 } 387 else { // reset the min and max 388 this.lastCategory = column; 389 this.min = value.doubleValue(); 390 this.max = value.doubleValue(); 391 } 392 393 // connect to the previous point 394 if (this.plotLines) { 395 if (column != 0) { 396 Number previousValue = dataset.getValue(row, column - 1); 397 if (previousValue != null) { 398 // previous data point... 399 double previous = previousValue.doubleValue(); 400 double x0 = domainAxis.getCategoryMiddle(column - 1, 401 getColumnCount(), dataArea, 402 plot.getDomainAxisEdge()); 403 double y0 = rangeAxis.valueToJava2D(previous, dataArea, 404 plot.getRangeAxisEdge()); 405 g2.setPaint(getItemPaint(row, column)); 406 g2.setStroke(getItemStroke(row, column)); 407 Line2D line; 408 if (orient == PlotOrientation.VERTICAL) { 409 line = new Line2D.Double(x0, y0, x1, y1); 410 } 411 else { 412 line = new Line2D.Double(y0, x0, y1, x1); 413 } 414 g2.draw(line); 415 } 416 } 417 } 418 419 // add an item entity, if this information is being collected 420 EntityCollection entities = state.getEntityCollection(); 421 if (entities != null && shape != null) { 422 addItemEntity(entities, dataset, row, column, shape); 423 } 424 } 425 } 426 427 /** 428 * Tests this instance for equality with an arbitrary object. The icon 429 * fields are NOT included in the test, so this implementation is a little 430 * weak. 431 * 432 * @param obj the object (<code>null</code> permitted). 433 * 434 * @return A boolean. 435 * 436 * @since 1.0.7 437 */ 438 public boolean equals(Object obj) { 439 if (obj == this) { 440 return true; 441 } 442 if (!(obj instanceof MinMaxCategoryRenderer)) { 443 return false; 444 } 445 MinMaxCategoryRenderer that = (MinMaxCategoryRenderer) obj; 446 if (this.plotLines != that.plotLines) { 447 return false; 448 } 449 if (!PaintUtilities.equal(this.groupPaint, that.groupPaint)) { 450 return false; 451 } 452 if (!this.groupStroke.equals(that.groupStroke)) { 453 return false; 454 } 455 return super.equals(obj); 456 } 457 458 /** 459 * Returns an icon. 460 * 461 * @param shape the shape. 462 * @param fillPaint the fill paint. 463 * @param outlinePaint the outline paint. 464 * 465 * @return The icon. 466 */ 467 private Icon getIcon(Shape shape, final Paint fillPaint, 468 final Paint outlinePaint) { 469 470 final int width = shape.getBounds().width; 471 final int height = shape.getBounds().height; 472 final GeneralPath path = new GeneralPath(shape); 473 return new Icon() { 474 public void paintIcon(Component c, Graphics g, int x, int y) { 475 Graphics2D g2 = (Graphics2D) g; 476 path.transform(AffineTransform.getTranslateInstance(x, y)); 477 if (fillPaint != null) { 478 g2.setPaint(fillPaint); 479 g2.fill(path); 480 } 481 if (outlinePaint != null) { 482 g2.setPaint(outlinePaint); 483 g2.draw(path); 484 } 485 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 486 } 487 488 public int getIconWidth() { 489 return width; 490 } 491 492 public int getIconHeight() { 493 return height; 494 } 495 496 }; 497 } 498 499 /** 500 * Returns an icon from a shape. 501 * 502 * @param shape the shape. 503 * @param fill the fill flag. 504 * @param outline the outline flag. 505 * 506 * @return The icon. 507 */ 508 private Icon getIcon(Shape shape, final boolean fill, 509 final boolean outline) { 510 final int width = shape.getBounds().width; 511 final int height = shape.getBounds().height; 512 final GeneralPath path = new GeneralPath(shape); 513 return new Icon() { 514 public void paintIcon(Component c, Graphics g, int x, int y) { 515 Graphics2D g2 = (Graphics2D) g; 516 path.transform(AffineTransform.getTranslateInstance(x, y)); 517 if (fill) { 518 g2.fill(path); 519 } 520 if (outline) { 521 g2.draw(path); 522 } 523 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 524 } 525 526 public int getIconWidth() { 527 return width; 528 } 529 530 public int getIconHeight() { 531 return height; 532 } 533 }; 534 } 535 536 /** 537 * Provides serialization support. 538 * 539 * @param stream the output stream. 540 * 541 * @throws IOException if there is an I/O error. 542 */ 543 private void writeObject(ObjectOutputStream stream) throws IOException { 544 stream.defaultWriteObject(); 545 SerialUtilities.writeStroke(this.groupStroke, stream); 546 SerialUtilities.writePaint(this.groupPaint, stream); 547 } 548 549 /** 550 * Provides serialization support. 551 * 552 * @param stream the input stream. 553 * 554 * @throws IOException if there is an I/O error. 555 * @throws ClassNotFoundException if there is a classpath problem. 556 */ 557 private void readObject(ObjectInputStream stream) 558 throws IOException, ClassNotFoundException { 559 stream.defaultReadObject(); 560 this.groupStroke = SerialUtilities.readStroke(stream); 561 this.groupPaint = SerialUtilities.readPaint(stream); 562 563 this.minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 564 Arc2D.OPEN), null, Color.black); 565 this.maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 566 Arc2D.OPEN), null, Color.black); 567 this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true); 568 } 569 570 }