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