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