001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * --------------------------- 028 * MinMaxCategoryRenderer.java 029 * --------------------------- 030 * (C) Copyright 2002-2014, 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 * 02-Jul-2013 : Use ParamChecks (DG); 058 * 059 */ 060 061package org.jfree.chart.renderer.category; 062 063import java.awt.BasicStroke; 064import java.awt.Color; 065import java.awt.Component; 066import java.awt.Graphics; 067import java.awt.Graphics2D; 068import java.awt.Paint; 069import java.awt.Shape; 070import java.awt.Stroke; 071import java.awt.geom.AffineTransform; 072import java.awt.geom.Arc2D; 073import java.awt.geom.GeneralPath; 074import java.awt.geom.Line2D; 075import java.awt.geom.Rectangle2D; 076import java.io.IOException; 077import java.io.ObjectInputStream; 078import java.io.ObjectOutputStream; 079 080import javax.swing.Icon; 081 082import org.jfree.chart.axis.CategoryAxis; 083import org.jfree.chart.axis.ValueAxis; 084import org.jfree.chart.entity.EntityCollection; 085import org.jfree.chart.event.RendererChangeEvent; 086import org.jfree.chart.plot.CategoryPlot; 087import org.jfree.chart.plot.PlotOrientation; 088import org.jfree.chart.util.ParamChecks; 089import org.jfree.data.category.CategoryDataset; 090import org.jfree.io.SerialUtilities; 091import org.jfree.util.PaintUtilities; 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. For use with the 097 * {@link CategoryPlot} class. The example shown here is generated by 098 * the <code>MinMaxCategoryPlotDemo1.java</code> program included in the 099 * JFreeChart Demo Collection: 100 * <br><br> 101 * <img src="../../../../../images/MinMaxCategoryRendererSample.png" 102 * alt="MinMaxCategoryRendererSample.png"> 103 */ 104public class MinMaxCategoryRenderer extends AbstractCategoryItemRenderer { 105 106 /** For serialization. */ 107 private static final long serialVersionUID = 2935615937671064911L; 108 109 /** A flag indicating whether or not lines are drawn between XY points. */ 110 private boolean plotLines = false; 111 112 /** 113 * The paint of the line between the minimum value and the maximum value. 114 */ 115 private transient Paint groupPaint = Color.black; 116 117 /** 118 * The stroke of the line between the minimum value and the maximum value. 119 */ 120 private transient Stroke groupStroke = new BasicStroke(1.0f); 121 122 /** The icon used to indicate the minimum value.*/ 123 private transient Icon minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 124 360, Arc2D.OPEN), null, Color.black); 125 126 /** The icon used to indicate the maximum value.*/ 127 private transient Icon maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 128 360, Arc2D.OPEN), null, Color.black); 129 130 /** The icon used to indicate the values.*/ 131 private transient Icon objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), 132 false, true); 133 134 /** The last category. */ 135 private int lastCategory = -1; 136 137 /** The minimum. */ 138 private double min; 139 140 /** The maximum. */ 141 private double max; 142 143 /** 144 * Default constructor. 145 */ 146 public MinMaxCategoryRenderer() { 147 super(); 148 } 149 150 /** 151 * Gets whether or not lines are drawn between category points. 152 * 153 * @return boolean true if line will be drawn between sequenced categories, 154 * otherwise false. 155 * 156 * @see #setDrawLines(boolean) 157 */ 158 public boolean isDrawLines() { 159 return this.plotLines; 160 } 161 162 /** 163 * Sets the flag that controls whether or not lines are drawn to connect 164 * the items within a series and sends a {@link RendererChangeEvent} to 165 * all registered listeners. 166 * 167 * @param draw the new value of the flag. 168 * 169 * @see #isDrawLines() 170 */ 171 public void setDrawLines(boolean draw) { 172 if (this.plotLines != draw) { 173 this.plotLines = draw; 174 fireChangeEvent(); 175 } 176 } 177 178 /** 179 * Returns the paint used to draw the line between the minimum and maximum 180 * value items in each category. 181 * 182 * @return The paint (never <code>null</code>). 183 * 184 * @see #setGroupPaint(Paint) 185 */ 186 public Paint getGroupPaint() { 187 return this.groupPaint; 188 } 189 190 /** 191 * Sets the paint used to draw the line between the minimum and maximum 192 * value items in each category and sends a {@link RendererChangeEvent} to 193 * all registered listeners. 194 * 195 * @param paint the paint (<code>null</code> not permitted). 196 * 197 * @see #getGroupPaint() 198 */ 199 public void setGroupPaint(Paint paint) { 200 ParamChecks.nullNotPermitted(paint, "paint"); 201 this.groupPaint = paint; 202 fireChangeEvent(); 203 } 204 205 /** 206 * Returns the stroke used to draw the line between the minimum and maximum 207 * value items in each category. 208 * 209 * @return The stroke (never <code>null</code>). 210 * 211 * @see #setGroupStroke(Stroke) 212 */ 213 public Stroke getGroupStroke() { 214 return this.groupStroke; 215 } 216 217 /** 218 * Sets the stroke of the line between the minimum value and the maximum 219 * value and sends a {@link RendererChangeEvent} to all registered 220 * listeners. 221 * 222 * @param stroke the new stroke (<code>null</code> not permitted). 223 */ 224 public void setGroupStroke(Stroke stroke) { 225 ParamChecks.nullNotPermitted(stroke, "stroke"); 226 this.groupStroke = stroke; 227 fireChangeEvent(); 228 } 229 230 /** 231 * Returns the icon drawn for each data item. 232 * 233 * @return The icon (never <code>null</code>). 234 * 235 * @see #setObjectIcon(Icon) 236 */ 237 public Icon getObjectIcon() { 238 return this.objectIcon; 239 } 240 241 /** 242 * Sets the icon drawn for each data item and sends a 243 * {@link RendererChangeEvent} to all registered listeners. 244 * 245 * @param icon the icon. 246 * 247 * @see #getObjectIcon() 248 */ 249 public void setObjectIcon(Icon icon) { 250 ParamChecks.nullNotPermitted(icon, "icon"); 251 this.objectIcon = icon; 252 fireChangeEvent(); 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 ParamChecks.nullNotPermitted(icon, "icon"); 278 this.maxIcon = icon; 279 fireChangeEvent(); 280 } 281 282 /** 283 * Returns the icon displayed for the minimum value data item within each 284 * category. 285 * 286 * @return The icon (never <code>null</code>). 287 * 288 * @see #setMinIcon(Icon) 289 */ 290 public Icon getMinIcon() { 291 return this.minIcon; 292 } 293 294 /** 295 * Sets the icon displayed for the minimum value data item within each 296 * category and sends a {@link RendererChangeEvent} to all registered 297 * listeners. 298 * 299 * @param icon the icon (<code>null</code> not permitted). 300 * 301 * @see #getMinIcon() 302 */ 303 public void setMinIcon(Icon icon) { 304 ParamChecks.nullNotPermitted(icon, "icon"); 305 this.minIcon = icon; 306 fireChangeEvent(); 307 } 308 309 /** 310 * Draw a single data item. 311 * 312 * @param g2 the graphics device. 313 * @param state the renderer state. 314 * @param dataArea the area in which the data is drawn. 315 * @param plot the plot. 316 * @param domainAxis the domain axis. 317 * @param rangeAxis the range axis. 318 * @param dataset the dataset. 319 * @param row the row index (zero-based). 320 * @param column the column index (zero-based). 321 * @param pass the pass index. 322 */ 323 @Override 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 Shape hotspot = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0); 338 339 g2.setPaint(getItemPaint(row, column)); 340 g2.setStroke(getItemStroke(row, column)); 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 // add an item entity, if this information is being collected 412 EntityCollection entities = state.getEntityCollection(); 413 if (entities != null) { 414 addItemEntity(entities, dataset, row, column, hotspot); 415 } 416 } 417 } 418 419 /** 420 * Tests this instance for equality with an arbitrary object. The icon 421 * fields are NOT included in the test, so this implementation is a little 422 * weak. 423 * 424 * @param obj the object (<code>null</code> permitted). 425 * 426 * @return A boolean. 427 * 428 * @since 1.0.7 429 */ 430 @Override 431 public boolean equals(Object obj) { 432 if (obj == this) { 433 return true; 434 } 435 if (!(obj instanceof MinMaxCategoryRenderer)) { 436 return false; 437 } 438 MinMaxCategoryRenderer that = (MinMaxCategoryRenderer) obj; 439 if (this.plotLines != that.plotLines) { 440 return false; 441 } 442 if (!PaintUtilities.equal(this.groupPaint, that.groupPaint)) { 443 return false; 444 } 445 if (!this.groupStroke.equals(that.groupStroke)) { 446 return false; 447 } 448 return super.equals(obj); 449 } 450 451 /** 452 * Returns an icon. 453 * 454 * @param shape the shape. 455 * @param fillPaint the fill paint. 456 * @param outlinePaint the outline paint. 457 * 458 * @return The icon. 459 */ 460 private Icon getIcon(Shape shape, final Paint fillPaint, 461 final Paint outlinePaint) { 462 463 final int width = shape.getBounds().width; 464 final int height = shape.getBounds().height; 465 final GeneralPath path = new GeneralPath(shape); 466 return new Icon() { 467 @Override 468 public void paintIcon(Component c, Graphics g, int x, int y) { 469 Graphics2D g2 = (Graphics2D) g; 470 path.transform(AffineTransform.getTranslateInstance(x, y)); 471 if (fillPaint != null) { 472 g2.setPaint(fillPaint); 473 g2.fill(path); 474 } 475 if (outlinePaint != null) { 476 g2.setPaint(outlinePaint); 477 g2.draw(path); 478 } 479 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 480 } 481 482 @Override 483 public int getIconWidth() { 484 return width; 485 } 486 487 @Override 488 public int getIconHeight() { 489 return height; 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 @Override 510 public void paintIcon(Component c, Graphics g, int x, int y) { 511 Graphics2D g2 = (Graphics2D) g; 512 path.transform(AffineTransform.getTranslateInstance(x, y)); 513 if (fill) { 514 g2.fill(path); 515 } 516 if (outline) { 517 g2.draw(path); 518 } 519 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 520 } 521 522 @Override 523 public int getIconWidth() { 524 return width; 525 } 526 527 @Override 528 public int getIconHeight() { 529 return height; 530 } 531 }; 532 } 533 534 /** 535 * Provides serialization support. 536 * 537 * @param stream the output stream. 538 * 539 * @throws IOException if there is an I/O error. 540 */ 541 private void writeObject(ObjectOutputStream stream) throws IOException { 542 stream.defaultWriteObject(); 543 SerialUtilities.writeStroke(this.groupStroke, stream); 544 SerialUtilities.writePaint(this.groupPaint, stream); 545 } 546 547 /** 548 * Provides serialization support. 549 * 550 * @param stream the input stream. 551 * 552 * @throws IOException if there is an I/O error. 553 * @throws ClassNotFoundException if there is a classpath problem. 554 */ 555 private void readObject(ObjectInputStream stream) 556 throws IOException, ClassNotFoundException { 557 stream.defaultReadObject(); 558 this.groupStroke = SerialUtilities.readStroke(stream); 559 this.groupPaint = SerialUtilities.readPaint(stream); 560 561 this.minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 562 Arc2D.OPEN), null, Color.black); 563 this.maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 360, 564 Arc2D.OPEN), null, Color.black); 565 this.objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), false, true); 566 } 567 568}