001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2005, 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-2005, 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.5 2005/12/02 10:05:57 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 * 056 */ 057 058 package org.jfree.chart.renderer.category; 059 060 import java.awt.BasicStroke; 061 import java.awt.Color; 062 import java.awt.Component; 063 import java.awt.Graphics; 064 import java.awt.Graphics2D; 065 import java.awt.Paint; 066 import java.awt.Shape; 067 import java.awt.Stroke; 068 import java.awt.geom.AffineTransform; 069 import java.awt.geom.Arc2D; 070 import java.awt.geom.GeneralPath; 071 import java.awt.geom.Line2D; 072 import java.awt.geom.Rectangle2D; 073 import java.io.IOException; 074 import java.io.ObjectInputStream; 075 import java.io.ObjectOutputStream; 076 077 import javax.swing.Icon; 078 079 import org.jfree.chart.axis.CategoryAxis; 080 import org.jfree.chart.axis.ValueAxis; 081 import org.jfree.chart.entity.CategoryItemEntity; 082 import org.jfree.chart.entity.EntityCollection; 083 import org.jfree.chart.event.RendererChangeEvent; 084 import org.jfree.chart.labels.CategoryToolTipGenerator; 085 import org.jfree.chart.plot.CategoryPlot; 086 import org.jfree.data.category.CategoryDataset; 087 import org.jfree.io.SerialUtilities; 088 089 /** 090 * Renderer for drawing min max plot. This renderer draws all the series under 091 * the same category in the same x position using <code>objectIcon</code> and 092 * a line from the maximum value to the minimum value. 093 * <p> 094 * For use with the {@link org.jfree.chart.plot.CategoryPlot} class. 095 * 096 * @author Tomer Peretz 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 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. 217 * 218 * @param groupStroke The new stroke 219 */ 220 public void setGroupStroke(Stroke groupStroke) { 221 this.groupStroke = groupStroke; 222 } 223 224 /** 225 * Returns the icon drawn for each data item. 226 * 227 * @return The icon (never <code>null</code>). 228 * 229 * @see #setObjectIcon(Icon) 230 */ 231 public Icon getObjectIcon() { 232 return this.objectIcon; 233 } 234 235 /** 236 * Sets the icon drawn for each data item. 237 * 238 * @param icon the icon. 239 * 240 * @see #getObjectIcon() 241 */ 242 public void setObjectIcon(Icon icon) { 243 if (icon == null) { 244 throw new IllegalArgumentException("Null 'icon' argument."); 245 } 246 this.objectIcon = icon; 247 notifyListeners(new RendererChangeEvent(this)); 248 } 249 250 /** 251 * Returns the icon displayed for the maximum value data item within each 252 * category. 253 * 254 * @return The icon (never <code>null</code>). 255 * 256 * @see #setMaxIcon(Icon) 257 */ 258 public Icon getMaxIcon() { 259 return this.maxIcon; 260 } 261 262 /** 263 * Sets the icon displayed for the maximum value data item within each 264 * category and sends a {@link RendererChangeEvent} to all registered 265 * listeners. 266 * 267 * @param icon the icon (<code>null</code> not permitted). 268 * 269 * @see #getMaxIcon() 270 */ 271 public void setMaxIcon(Icon icon) { 272 if (icon == null) { 273 throw new IllegalArgumentException("Null 'icon' argument."); 274 } 275 this.maxIcon = icon; 276 notifyListeners(new RendererChangeEvent(this)); 277 } 278 279 /** 280 * Returns the icon displayed for the minimum value data item within each 281 * category. 282 * 283 * @return The icon (never <code>null</code>). 284 * 285 * @see #setMinIcon(Icon) 286 */ 287 public Icon getMinIcon() { 288 return this.minIcon; 289 } 290 291 /** 292 * Sets the icon displayed for the minimum value data item within each 293 * category and sends a {@link RendererChangeEvent} to all registered 294 * listeners. 295 * 296 * @param icon the icon (<code>null</code> not permitted). 297 * 298 * @see #getMinIcon() 299 */ 300 public void setMinIcon(Icon icon) { 301 if (icon == null) { 302 throw new IllegalArgumentException("Null 'icon' argument."); 303 } 304 this.minIcon = icon; 305 notifyListeners(new RendererChangeEvent(this)); 306 } 307 308 /** 309 * Draw a single data item. 310 * 311 * @param g2 the graphics device. 312 * @param state the renderer state. 313 * @param dataArea the area in which the data is drawn. 314 * @param plot the plot. 315 * @param domainAxis the domain axis. 316 * @param rangeAxis the range axis. 317 * @param dataset the dataset. 318 * @param row the row index (zero-based). 319 * @param column the column index (zero-based). 320 * @param pass the pass index. 321 */ 322 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 323 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 324 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 325 int pass) { 326 327 // first check the number we are plotting... 328 Number value = dataset.getValue(row, column); 329 if (value != null) { 330 // current data point... 331 double x1 = domainAxis.getCategoryMiddle( 332 column, getColumnCount(), dataArea, plot.getDomainAxisEdge()); 333 double y1 = rangeAxis.valueToJava2D( 334 value.doubleValue(), dataArea, plot.getRangeAxisEdge()); 335 g2.setPaint(getItemPaint(row, column)); 336 g2.setStroke(getItemStroke(row, column)); 337 Shape shape = null; 338 shape = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0); 339 this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1); 340 if (this.lastCategory == column) { 341 if (this.min > value.doubleValue()) { 342 this.min = value.doubleValue(); 343 } 344 if (this.max < value.doubleValue()) { 345 this.max = value.doubleValue(); 346 } 347 if (dataset.getRowCount() - 1 == row) { 348 g2.setPaint(this.groupPaint); 349 g2.setStroke(this.groupStroke); 350 double minY = rangeAxis.valueToJava2D(this.min, dataArea, 351 plot.getRangeAxisEdge()); 352 double maxY = rangeAxis.valueToJava2D(this.max, dataArea, 353 plot.getRangeAxisEdge()); 354 g2.draw(new Line2D.Double(x1, minY, x1, maxY)); 355 this.minIcon.paintIcon(null, g2, (int) x1, (int) minY); 356 this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY); 357 } 358 } 359 else { // reset the min and max 360 this.lastCategory = column; 361 this.min = value.doubleValue(); 362 this.max = value.doubleValue(); 363 } 364 // connect to the previous point 365 if (this.plotLines) { 366 if (column != 0) { 367 Number previousValue = dataset.getValue(row, column - 1); 368 if (previousValue != null) { 369 // previous data point... 370 double previous = previousValue.doubleValue(); 371 double x0 = domainAxis.getCategoryMiddle( 372 column - 1, getColumnCount(), dataArea, 373 plot.getDomainAxisEdge()); 374 double y0 = rangeAxis.valueToJava2D( 375 previous, dataArea, plot.getRangeAxisEdge()); 376 g2.setPaint(getItemPaint(row, column)); 377 g2.setStroke(getItemStroke(row, column)); 378 Line2D line = new Line2D.Double(x0, y0, x1, y1); 379 g2.draw(line); 380 } 381 } 382 } 383 384 // collect entity and tool tip information... 385 if (state.getInfo() != null) { 386 EntityCollection entities = state.getEntityCollection(); 387 if (entities != null && shape != null) { 388 String tip = null; 389 CategoryToolTipGenerator tipster 390 = getToolTipGenerator(row, column); 391 if (tipster != null) { 392 tip = tipster.generateToolTip(dataset, row, column); 393 } 394 CategoryItemEntity entity = new CategoryItemEntity( 395 shape, tip, null, dataset, row, 396 dataset.getColumnKey(column), column); 397 entities.add(entity); 398 } 399 } 400 } 401 } 402 403 /** 404 * Returns an icon. 405 * 406 * @param shape the shape. 407 * @param fillPaint the fill paint. 408 * @param outlinePaint the outline paint. 409 * 410 * @return The icon. 411 */ 412 private Icon getIcon(Shape shape, final Paint fillPaint, 413 final Paint outlinePaint) { 414 415 final int width = shape.getBounds().width; 416 final int height = shape.getBounds().height; 417 final GeneralPath path = new GeneralPath(shape); 418 return new Icon() { 419 public void paintIcon(Component c, Graphics g, int x, int y) { 420 Graphics2D g2 = (Graphics2D) g; 421 path.transform(AffineTransform.getTranslateInstance(x, y)); 422 if (fillPaint != null) { 423 g2.setPaint(fillPaint); 424 g2.fill(path); 425 } 426 if (outlinePaint != null) { 427 g2.setPaint(outlinePaint); 428 g2.draw(path); 429 } 430 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 431 } 432 433 public int getIconWidth() { 434 return width; 435 } 436 437 public int getIconHeight() { 438 return height; 439 } 440 441 }; 442 } 443 444 /** 445 * Returns an icon. 446 * 447 * @param shape the shape. 448 * @param fill the fill flag. 449 * @param outline the outline flag. 450 * 451 * @return The icon. 452 */ 453 private Icon getIcon(Shape shape, final boolean fill, 454 final boolean outline) { 455 final int width = shape.getBounds().width; 456 final int height = shape.getBounds().height; 457 final GeneralPath path = new GeneralPath(shape); 458 return new Icon() { 459 public void paintIcon(Component c, Graphics g, int x, int y) { 460 Graphics2D g2 = (Graphics2D) g; 461 path.transform(AffineTransform.getTranslateInstance(x, y)); 462 if (fill) { 463 g2.fill(path); 464 } 465 if (outline) { 466 g2.draw(path); 467 } 468 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 469 } 470 471 public int getIconWidth() { 472 return width; 473 } 474 475 public int getIconHeight() { 476 return height; 477 } 478 }; 479 } 480 481 /** 482 * Provides serialization support. 483 * 484 * @param stream the output stream. 485 * 486 * @throws IOException if there is an I/O error. 487 */ 488 private void writeObject(ObjectOutputStream stream) throws IOException { 489 stream.defaultWriteObject(); 490 SerialUtilities.writeStroke(this.groupStroke, stream); 491 SerialUtilities.writePaint(this.groupPaint, stream); 492 } 493 494 /** 495 * Provides serialization support. 496 * 497 * @param stream the input stream. 498 * 499 * @throws IOException if there is an I/O error. 500 * @throws ClassNotFoundException if there is a classpath problem. 501 */ 502 private void readObject(ObjectInputStream stream) 503 throws IOException, ClassNotFoundException { 504 stream.defaultReadObject(); 505 this.groupStroke = SerialUtilities.readStroke(stream); 506 this.groupPaint = SerialUtilities.readPaint(stream); 507 508 this.minIcon = getIcon( 509 new Arc2D.Double(-4, -4, 8, 8, 0, 360, Arc2D.OPEN), null, 510 Color.black); 511 this.maxIcon = getIcon( 512 new Arc2D.Double(-4, -4, 8, 8, 0, 360, Arc2D.OPEN), null, 513 Color.black); 514 this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true); 515 } 516 517 }