001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, by David Gilbert. 031 * 032 * Original Author: Tomer Peretz; 033 * Contributor(s): David Gilbert; 034 * Christian W. Zuckschwerdt; 035 * Nicolas Brodu (for Astrium and EADS Corporate Research 036 * Center); 037 */ 038 039package org.jfree.chart.renderer.category; 040 041import java.awt.BasicStroke; 042import java.awt.Color; 043import java.awt.Component; 044import java.awt.Graphics; 045import java.awt.Graphics2D; 046import java.awt.Paint; 047import java.awt.Shape; 048import java.awt.Stroke; 049import java.awt.geom.AffineTransform; 050import java.awt.geom.Arc2D; 051import java.awt.geom.GeneralPath; 052import java.awt.geom.Line2D; 053import java.awt.geom.Rectangle2D; 054import java.io.IOException; 055import java.io.ObjectInputStream; 056import java.io.ObjectOutputStream; 057 058import javax.swing.Icon; 059 060import org.jfree.chart.axis.CategoryAxis; 061import org.jfree.chart.axis.ValueAxis; 062import org.jfree.chart.entity.EntityCollection; 063import org.jfree.chart.event.RendererChangeEvent; 064import org.jfree.chart.plot.CategoryPlot; 065import org.jfree.chart.plot.PlotOrientation; 066import org.jfree.chart.util.PaintUtils; 067import org.jfree.chart.util.Args; 068import org.jfree.chart.util.SerialUtils; 069import org.jfree.data.category.CategoryDataset; 070 071/** 072 * Renderer for drawing min max plot. This renderer draws all the series under 073 * the same category in the same x position using {@code objectIcon} and 074 * a line from the maximum value to the minimum value. For use with the 075 * {@link CategoryPlot} class. The example shown here is generated by 076 * the {@code MinMaxCategoryPlotDemo1.java} program included in the 077 * JFreeChart Demo Collection: 078 * <br><br> 079 * <img src="doc-files/MinMaxCategoryRendererSample.png" 080 * alt="MinMaxCategoryRendererSample.png"> 081 */ 082public class MinMaxCategoryRenderer extends AbstractCategoryItemRenderer { 083 084 /** For serialization. */ 085 private static final long serialVersionUID = 2935615937671064911L; 086 087 /** A flag indicating whether or not lines are drawn between XY points. */ 088 private boolean plotLines = false; 089 090 /** 091 * The paint of the line between the minimum value and the maximum value. 092 */ 093 private transient Paint groupPaint = Color.BLACK; 094 095 /** 096 * The stroke of the line between the minimum value and the maximum value. 097 */ 098 private transient Stroke groupStroke = new BasicStroke(1.0f); 099 100 /** The icon used to indicate the minimum value.*/ 101 private transient Icon minIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 102 360, Arc2D.OPEN), null, Color.BLACK); 103 104 /** The icon used to indicate the maximum value.*/ 105 private transient Icon maxIcon = getIcon(new Arc2D.Double(-4, -4, 8, 8, 0, 106 360, Arc2D.OPEN), null, Color.BLACK); 107 108 /** The icon used to indicate the values.*/ 109 private transient Icon objectIcon = getIcon(new Line2D.Double(-4, 0, 4, 0), 110 false, true); 111 112 /** The last category. */ 113 private int lastCategory = -1; 114 115 /** The minimum. */ 116 private double min; 117 118 /** The maximum. */ 119 private double max; 120 121 /** 122 * Default constructor. 123 */ 124 public MinMaxCategoryRenderer() { 125 super(); 126 } 127 128 /** 129 * Gets whether or not lines are drawn between category points. 130 * 131 * @return boolean true if line will be drawn between sequenced categories, 132 * otherwise false. 133 * 134 * @see #setDrawLines(boolean) 135 */ 136 public boolean isDrawLines() { 137 return this.plotLines; 138 } 139 140 /** 141 * Sets the flag that controls whether or not lines are drawn to connect 142 * the items within a series and sends a {@link RendererChangeEvent} to 143 * all registered listeners. 144 * 145 * @param draw the new value of the flag. 146 * 147 * @see #isDrawLines() 148 */ 149 public void setDrawLines(boolean draw) { 150 if (this.plotLines != draw) { 151 this.plotLines = draw; 152 fireChangeEvent(); 153 } 154 } 155 156 /** 157 * Returns the paint used to draw the line between the minimum and maximum 158 * value items in each category. 159 * 160 * @return The paint (never {@code null}). 161 * 162 * @see #setGroupPaint(Paint) 163 */ 164 public Paint getGroupPaint() { 165 return this.groupPaint; 166 } 167 168 /** 169 * Sets the paint used to draw the line between the minimum and maximum 170 * value items in each category and sends a {@link RendererChangeEvent} to 171 * all registered listeners. 172 * 173 * @param paint the paint ({@code null} not permitted). 174 * 175 * @see #getGroupPaint() 176 */ 177 public void setGroupPaint(Paint paint) { 178 Args.nullNotPermitted(paint, "paint"); 179 this.groupPaint = paint; 180 fireChangeEvent(); 181 } 182 183 /** 184 * Returns the stroke used to draw the line between the minimum and maximum 185 * value items in each category. 186 * 187 * @return The stroke (never {@code null}). 188 * 189 * @see #setGroupStroke(Stroke) 190 */ 191 public Stroke getGroupStroke() { 192 return this.groupStroke; 193 } 194 195 /** 196 * Sets the stroke of the line between the minimum value and the maximum 197 * value and sends a {@link RendererChangeEvent} to all registered 198 * listeners. 199 * 200 * @param stroke the new stroke ({@code null} not permitted). 201 */ 202 public void setGroupStroke(Stroke stroke) { 203 Args.nullNotPermitted(stroke, "stroke"); 204 this.groupStroke = stroke; 205 fireChangeEvent(); 206 } 207 208 /** 209 * Returns the icon drawn for each data item. 210 * 211 * @return The icon (never {@code null}). 212 * 213 * @see #setObjectIcon(Icon) 214 */ 215 public Icon getObjectIcon() { 216 return this.objectIcon; 217 } 218 219 /** 220 * Sets the icon drawn for each data item and sends a 221 * {@link RendererChangeEvent} to all registered listeners. 222 * 223 * @param icon the icon. 224 * 225 * @see #getObjectIcon() 226 */ 227 public void setObjectIcon(Icon icon) { 228 Args.nullNotPermitted(icon, "icon"); 229 this.objectIcon = icon; 230 fireChangeEvent(); 231 } 232 233 /** 234 * Returns the icon displayed for the maximum value data item within each 235 * category. 236 * 237 * @return The icon (never {@code null}). 238 * 239 * @see #setMaxIcon(Icon) 240 */ 241 public Icon getMaxIcon() { 242 return this.maxIcon; 243 } 244 245 /** 246 * Sets the icon displayed for the maximum value data item within each 247 * category and sends a {@link RendererChangeEvent} to all registered 248 * listeners. 249 * 250 * @param icon the icon ({@code null} not permitted). 251 * 252 * @see #getMaxIcon() 253 */ 254 public void setMaxIcon(Icon icon) { 255 Args.nullNotPermitted(icon, "icon"); 256 this.maxIcon = icon; 257 fireChangeEvent(); 258 } 259 260 /** 261 * Returns the icon displayed for the minimum value data item within each 262 * category. 263 * 264 * @return The icon (never {@code null}). 265 * 266 * @see #setMinIcon(Icon) 267 */ 268 public Icon getMinIcon() { 269 return this.minIcon; 270 } 271 272 /** 273 * Sets the icon displayed for the minimum 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} not permitted). 278 * 279 * @see #getMinIcon() 280 */ 281 public void setMinIcon(Icon icon) { 282 Args.nullNotPermitted(icon, "icon"); 283 this.minIcon = icon; 284 fireChangeEvent(); 285 } 286 287 /** 288 * Draw a single data item. 289 * 290 * @param g2 the graphics device. 291 * @param state the renderer state. 292 * @param dataArea the area in which the data is drawn. 293 * @param plot the plot. 294 * @param domainAxis the domain axis. 295 * @param rangeAxis the range axis. 296 * @param dataset the dataset. 297 * @param row the row index (zero-based). 298 * @param column the column index (zero-based). 299 * @param pass the pass index. 300 */ 301 @Override 302 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 303 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 304 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 305 int pass) { 306 307 // first check the number we are plotting... 308 Number value = dataset.getValue(row, column); 309 if (value != null) { 310 // current data point... 311 double x1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 312 dataArea, plot.getDomainAxisEdge()); 313 double y1 = rangeAxis.valueToJava2D(value.doubleValue(), dataArea, 314 plot.getRangeAxisEdge()); 315 Shape hotspot = new Rectangle2D.Double(x1 - 4, y1 - 4, 8.0, 8.0); 316 317 g2.setPaint(getItemPaint(row, column)); 318 g2.setStroke(getItemStroke(row, column)); 319 320 PlotOrientation orient = plot.getOrientation(); 321 if (orient == PlotOrientation.VERTICAL) { 322 this.objectIcon.paintIcon(null, g2, (int) x1, (int) y1); 323 } 324 else { 325 this.objectIcon.paintIcon(null, g2, (int) y1, (int) x1); 326 } 327 328 if (this.lastCategory == column) { 329 if (this.min > value.doubleValue()) { 330 this.min = value.doubleValue(); 331 } 332 if (this.max < value.doubleValue()) { 333 this.max = value.doubleValue(); 334 } 335 336 // last series, so we are ready to draw the min and max 337 if (dataset.getRowCount() - 1 == row) { 338 g2.setPaint(this.groupPaint); 339 g2.setStroke(this.groupStroke); 340 double minY = rangeAxis.valueToJava2D(this.min, dataArea, 341 plot.getRangeAxisEdge()); 342 double maxY = rangeAxis.valueToJava2D(this.max, dataArea, 343 plot.getRangeAxisEdge()); 344 345 if (orient == PlotOrientation.VERTICAL) { 346 g2.draw(new Line2D.Double(x1, minY, x1, maxY)); 347 this.minIcon.paintIcon(null, g2, (int) x1, (int) minY); 348 this.maxIcon.paintIcon(null, g2, (int) x1, (int) maxY); 349 } 350 else { 351 g2.draw(new Line2D.Double(minY, x1, maxY, x1)); 352 this.minIcon.paintIcon(null, g2, (int) minY, (int) x1); 353 this.maxIcon.paintIcon(null, g2, (int) maxY, (int) x1); 354 } 355 } 356 } 357 else { // reset the min and max 358 this.lastCategory = column; 359 this.min = value.doubleValue(); 360 this.max = value.doubleValue(); 361 } 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(column - 1, 371 getColumnCount(), dataArea, 372 plot.getDomainAxisEdge()); 373 double y0 = rangeAxis.valueToJava2D(previous, dataArea, 374 plot.getRangeAxisEdge()); 375 g2.setPaint(getItemPaint(row, column)); 376 g2.setStroke(getItemStroke(row, column)); 377 Line2D line; 378 if (orient == PlotOrientation.VERTICAL) { 379 line = new Line2D.Double(x0, y0, x1, y1); 380 } 381 else { 382 line = new Line2D.Double(y0, x0, y1, x1); 383 } 384 g2.draw(line); 385 } 386 } 387 } 388 389 // add an item entity, if this information is being collected 390 EntityCollection entities = state.getEntityCollection(); 391 if (entities != null) { 392 addItemEntity(entities, dataset, row, column, hotspot); 393 } 394 } 395 } 396 397 /** 398 * Tests this instance for equality with an arbitrary object. The icon 399 * fields are NOT included in the test, so this implementation is a little 400 * weak. 401 * 402 * @param obj the object ({@code null} permitted). 403 * 404 * @return A boolean. 405 */ 406 @Override 407 public boolean equals(Object obj) { 408 if (obj == this) { 409 return true; 410 } 411 if (!(obj instanceof MinMaxCategoryRenderer)) { 412 return false; 413 } 414 MinMaxCategoryRenderer that = (MinMaxCategoryRenderer) obj; 415 if (this.plotLines != that.plotLines) { 416 return false; 417 } 418 if (!PaintUtils.equal(this.groupPaint, that.groupPaint)) { 419 return false; 420 } 421 if (!this.groupStroke.equals(that.groupStroke)) { 422 return false; 423 } 424 return super.equals(obj); 425 } 426 427 /** 428 * Returns an icon. 429 * 430 * @param shape the shape. 431 * @param fillPaint the fill paint. 432 * @param outlinePaint the outline paint. 433 * 434 * @return The icon. 435 */ 436 private Icon getIcon(Shape shape, final Paint fillPaint, final Paint outlinePaint) { 437 438 final int width = shape.getBounds().width; 439 final int height = shape.getBounds().height; 440 final GeneralPath path = new GeneralPath(shape); 441 return new Icon() { 442 @Override 443 public void paintIcon(Component c, Graphics g, int x, int y) { 444 Graphics2D g2 = (Graphics2D) g; 445 path.transform(AffineTransform.getTranslateInstance(x, y)); 446 if (fillPaint != null) { 447 g2.setPaint(fillPaint); 448 g2.fill(path); 449 } 450 if (outlinePaint != null) { 451 g2.setPaint(outlinePaint); 452 g2.draw(path); 453 } 454 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 455 } 456 457 @Override 458 public int getIconWidth() { 459 return width; 460 } 461 462 @Override 463 public int getIconHeight() { 464 return height; 465 } 466 }; 467 } 468 469 /** 470 * Returns an icon from a shape. 471 * 472 * @param shape the shape. 473 * @param fill the fill flag. 474 * @param outline the outline flag. 475 * 476 * @return The icon. 477 */ 478 private Icon getIcon(Shape shape, final boolean fill, final boolean outline) { 479 final int width = shape.getBounds().width; 480 final int height = shape.getBounds().height; 481 final GeneralPath path = new GeneralPath(shape); 482 return new Icon() { 483 @Override 484 public void paintIcon(Component c, Graphics g, int x, int y) { 485 Graphics2D g2 = (Graphics2D) g; 486 path.transform(AffineTransform.getTranslateInstance(x, y)); 487 if (fill) { 488 g2.fill(path); 489 } 490 if (outline) { 491 g2.draw(path); 492 } 493 path.transform(AffineTransform.getTranslateInstance(-x, -y)); 494 } 495 496 @Override 497 public int getIconWidth() { 498 return width; 499 } 500 501 @Override 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 SerialUtils.writeStroke(this.groupStroke, stream); 518 SerialUtils.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 = SerialUtils.readStroke(stream); 533 this.groupPaint = SerialUtils.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}