001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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 * XYShapeRenderer.java 029 * -------------------- 030 * (C) Copyright 2008-2011 by Andreas Haumer, xS+S and Contributors. 031 * 032 * Original Author: Martin Hoeller (x Software + Systeme xS+S - Andreas 033 * Haumer); 034 * Contributor(s): David Gilbert (for Object Refinery Limited); 035 * 036 * Changes: 037 * -------- 038 * 17-Sep-2008 : Version 1, based on a contribution from Martin Hoeller with 039 * amendments by David Gilbert (DG); 040 * 16-Feb-2010 : Added findZBounds() (patch 2952086) (MH); 041 * 19-Oct-2011 : Fixed NPE in findRangeBounds() (bug 3026341) (DG); 042 * 043 */ 044 045package org.jfree.chart.renderer.xy; 046 047import java.awt.BasicStroke; 048import java.awt.Color; 049import java.awt.Graphics2D; 050import java.awt.Paint; 051import java.awt.Shape; 052import java.awt.Stroke; 053import java.awt.geom.Ellipse2D; 054import java.awt.geom.Line2D; 055import java.awt.geom.Rectangle2D; 056import java.io.IOException; 057import java.io.ObjectInputStream; 058import java.io.ObjectOutputStream; 059import java.io.Serializable; 060 061import org.jfree.chart.axis.ValueAxis; 062import org.jfree.chart.entity.EntityCollection; 063import org.jfree.chart.event.RendererChangeEvent; 064import org.jfree.chart.plot.CrosshairState; 065import org.jfree.chart.plot.PlotOrientation; 066import org.jfree.chart.plot.PlotRenderingInfo; 067import org.jfree.chart.plot.XYPlot; 068import org.jfree.chart.renderer.LookupPaintScale; 069import org.jfree.chart.renderer.PaintScale; 070import org.jfree.data.Range; 071import org.jfree.data.general.DatasetUtilities; 072import org.jfree.data.xy.XYDataset; 073import org.jfree.data.xy.XYZDataset; 074import org.jfree.io.SerialUtilities; 075import org.jfree.util.PublicCloneable; 076import org.jfree.util.ShapeUtilities; 077 078/** 079 * A renderer that draws shapes at (x, y) coordinates and, if the dataset 080 * is an instance of {@link XYZDataset}, fills the shapes with a paint that 081 * is based on the z-value (the paint is obtained from a lookup table). The 082 * renderer also allows for optional guidelines, horizontal and vertical lines 083 * connecting the shape to the edges of the plot. 084 * <br><br> 085 * The example shown here is generated by the 086 * <code>XYShapeRendererDemo1.java</code> program included in the JFreeChart 087 * demo collection: 088 * <br><br> 089 * <img src="../../../../../images/XYShapeRendererSample.png" 090 * alt="XYShapeRendererSample.png" /> 091 * <br><br> 092 * This renderer has similarities to, but also differences from, the 093 * {@link XYLineAndShapeRenderer}. 094 * 095 * @since 1.0.11 096 */ 097public class XYShapeRenderer extends AbstractXYItemRenderer 098 implements XYItemRenderer, Cloneable, Serializable { 099 100 /** Auto generated serial version id. */ 101 private static final long serialVersionUID = 8320552104211173221L; 102 103 /** The paint scale (never null). */ 104 private PaintScale paintScale; 105 106 /** A flag that controls whether or not the shape outlines are drawn. */ 107 private boolean drawOutlines; 108 109 /** 110 * A flag that controls whether or not the outline paint is used (if not, 111 * the regular paint is used). 112 */ 113 private boolean useOutlinePaint; 114 115 /** 116 * A flag that controls whether or not the fill paint is used (if not, 117 * the fill paint is used). 118 */ 119 private boolean useFillPaint; 120 121 /** Flag indicating if guide lines should be drawn for every item. */ 122 private boolean guideLinesVisible; 123 124 /** The paint used for drawing the guide lines (never null). */ 125 private transient Paint guideLinePaint; 126 127 /** The stroke used for drawing the guide lines (never null). */ 128 private transient Stroke guideLineStroke; 129 130 /** 131 * Creates a new <code>XYShapeRenderer</code> instance with default 132 * attributes. 133 */ 134 public XYShapeRenderer() { 135 this.paintScale = new LookupPaintScale(); 136 this.useFillPaint = false; 137 this.drawOutlines = false; 138 this.useOutlinePaint = true; 139 this.guideLinesVisible = false; 140 this.guideLinePaint = Color.darkGray; 141 this.guideLineStroke = new BasicStroke(); 142 setBaseShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0)); 143 setAutoPopulateSeriesShape(false); 144 } 145 146 /** 147 * Returns the paint scale used by the renderer. 148 * 149 * @return The paint scale (never <code>null</code>). 150 * 151 * @see #setPaintScale(PaintScale) 152 */ 153 public PaintScale getPaintScale() { 154 return this.paintScale; 155 } 156 157 /** 158 * Sets the paint scale used by the renderer and sends a 159 * {@link RendererChangeEvent} to all registered listeners. 160 * 161 * @param scale the scale (<code>null</code> not permitted). 162 * 163 * @see #getPaintScale() 164 */ 165 public void setPaintScale(PaintScale scale) { 166 if (scale == null) { 167 throw new IllegalArgumentException("Null 'scale' argument."); 168 } 169 this.paintScale = scale; 170 notifyListeners(new RendererChangeEvent(this)); 171 } 172 173 /** 174 * Returns <code>true</code> if outlines should be drawn for shapes, and 175 * <code>false</code> otherwise. 176 * 177 * @return A boolean. 178 * 179 * @see #setDrawOutlines(boolean) 180 */ 181 public boolean getDrawOutlines() { 182 return this.drawOutlines; 183 } 184 185 /** 186 * Sets the flag that controls whether outlines are drawn for 187 * shapes, and sends a {@link RendererChangeEvent} to all registered 188 * listeners. 189 * <P> 190 * In some cases, shapes look better if they do NOT have an outline, but 191 * this flag allows you to set your own preference. 192 * 193 * @param flag the flag. 194 * 195 * @see #getDrawOutlines() 196 */ 197 public void setDrawOutlines(boolean flag) { 198 this.drawOutlines = flag; 199 fireChangeEvent(); 200 } 201 202 /** 203 * Returns <code>true</code> if the renderer should use the fill paint 204 * setting to fill shapes, and <code>false</code> if it should just 205 * use the regular paint. 206 * <p> 207 * Refer to <code>XYLineAndShapeRendererDemo2.java</code> to see the 208 * effect of this flag. 209 * 210 * @return A boolean. 211 * 212 * @see #setUseFillPaint(boolean) 213 * @see #getUseOutlinePaint() 214 */ 215 public boolean getUseFillPaint() { 216 return this.useFillPaint; 217 } 218 219 /** 220 * Sets the flag that controls whether the fill paint is used to fill 221 * shapes, and sends a {@link RendererChangeEvent} to all 222 * registered listeners. 223 * 224 * @param flag the flag. 225 * 226 * @see #getUseFillPaint() 227 */ 228 public void setUseFillPaint(boolean flag) { 229 this.useFillPaint = flag; 230 fireChangeEvent(); 231 } 232 233 /** 234 * Returns the flag that controls whether the outline paint is used for 235 * shape outlines. If not, the regular series paint is used. 236 * 237 * @return A boolean. 238 * 239 * @see #setUseOutlinePaint(boolean) 240 */ 241 public boolean getUseOutlinePaint() { 242 return this.useOutlinePaint; 243 } 244 245 /** 246 * Sets the flag that controls whether the outline paint is used for shape 247 * outlines, and sends a {@link RendererChangeEvent} to all registered 248 * listeners. 249 * 250 * @param use the flag. 251 * 252 * @see #getUseOutlinePaint() 253 */ 254 public void setUseOutlinePaint(boolean use) { 255 this.useOutlinePaint = use; 256 fireChangeEvent(); 257 } 258 259 /** 260 * Returns a flag that controls whether or not guide lines are drawn for 261 * each data item (the lines are horizontal and vertical "crosshairs" 262 * linking the data point to the axes). 263 * 264 * @return A boolean. 265 * 266 * @see #setGuideLinesVisible(boolean) 267 */ 268 public boolean isGuideLinesVisible() { 269 return this.guideLinesVisible; 270 } 271 272 /** 273 * Sets the flag that controls whether or not guide lines are drawn for 274 * each data item and sends a {@link RendererChangeEvent} to all registered 275 * listeners. 276 * 277 * @param visible the new flag value. 278 * 279 * @see #isGuideLinesVisible() 280 */ 281 public void setGuideLinesVisible(boolean visible) { 282 this.guideLinesVisible = visible; 283 fireChangeEvent(); 284 } 285 286 /** 287 * Returns the paint used to draw the guide lines. 288 * 289 * @return The paint (never <code>null</code>). 290 * 291 * @see #setGuideLinePaint(Paint) 292 */ 293 public Paint getGuideLinePaint() { 294 return this.guideLinePaint; 295 } 296 297 /** 298 * Sets the paint used to draw the guide lines and sends a 299 * {@link RendererChangeEvent} to all registered listeners. 300 * 301 * @param paint the paint (<code>null</code> not permitted). 302 * 303 * @see #getGuideLinePaint() 304 */ 305 public void setGuideLinePaint(Paint paint) { 306 if (paint == null) { 307 throw new IllegalArgumentException("Null 'paint' argument."); 308 } 309 this.guideLinePaint = paint; 310 fireChangeEvent(); 311 } 312 313 /** 314 * Returns the stroke used to draw the guide lines. 315 * 316 * @return The stroke. 317 * 318 * @see #setGuideLineStroke(Stroke) 319 */ 320 public Stroke getGuideLineStroke() { 321 return this.guideLineStroke; 322 } 323 324 /** 325 * Sets the stroke used to draw the guide lines and sends a 326 * {@link RendererChangeEvent} to all registered listeners. 327 * 328 * @param stroke the stroke (<code>null</code> not permitted). 329 * 330 * @see #getGuideLineStroke() 331 */ 332 public void setGuideLineStroke(Stroke stroke) { 333 if (stroke == null) { 334 throw new IllegalArgumentException("Null 'stroke' argument."); 335 } 336 this.guideLineStroke = stroke; 337 fireChangeEvent(); 338 } 339 340 /** 341 * Returns the lower and upper bounds (range) of the x-values in the 342 * specified dataset. 343 * 344 * @param dataset the dataset (<code>null</code> permitted). 345 * 346 * @return The range (<code>null</code> if the dataset is <code>null</code> 347 * or empty). 348 */ 349 public Range findDomainBounds(XYDataset dataset) { 350 if (dataset == null) { 351 return null; 352 } 353 Range r = DatasetUtilities.findDomainBounds(dataset, false); 354 if (r == null) { 355 return null; 356 } 357 double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2; 358 return new Range(r.getLowerBound() + offset, 359 r.getUpperBound() + offset); 360 } 361 362 /** 363 * Returns the range of values the renderer requires to display all the 364 * items from the specified dataset. 365 * 366 * @param dataset the dataset (<code>null</code> permitted). 367 * 368 * @return The range (<code>null</code> if the dataset is <code>null</code> 369 * or empty). 370 */ 371 public Range findRangeBounds(XYDataset dataset) { 372 if (dataset == null) { 373 return null; 374 } 375 Range r = DatasetUtilities.findRangeBounds(dataset, false); 376 if (r == null) { 377 return null; 378 } 379 double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2; 380 return new Range(r.getLowerBound() + offset, r.getUpperBound() 381 + offset); 382 } 383 384 /** 385 * Return the range of z-values in the specified dataset. 386 * 387 * @param dataset the dataset (<code>null</code> permitted). 388 * 389 * @return The range (<code>null</code> if the dataset is <code>null</code> 390 * or empty). 391 */ 392 public Range findZBounds(XYZDataset dataset) { 393 if (dataset != null) { 394 return DatasetUtilities.findZBounds(dataset); 395 } 396 else { 397 return null; 398 } 399 } 400 401 /** 402 * Returns the number of passes required by this renderer. 403 * 404 * @return <code>2</code>. 405 */ 406 public int getPassCount() { 407 return 2; 408 } 409 410 /** 411 * Draws the block representing the specified item. 412 * 413 * @param g2 the graphics device. 414 * @param state the state. 415 * @param dataArea the data area. 416 * @param info the plot rendering info. 417 * @param plot the plot. 418 * @param domainAxis the x-axis. 419 * @param rangeAxis the y-axis. 420 * @param dataset the dataset. 421 * @param series the series index. 422 * @param item the item index. 423 * @param crosshairState the crosshair state. 424 * @param pass the pass index. 425 */ 426 public void drawItem(Graphics2D g2, XYItemRendererState state, 427 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 428 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 429 int series, int item, CrosshairState crosshairState, int pass) { 430 431 Shape hotspot = null; 432 EntityCollection entities = null; 433 if (info != null) { 434 entities = info.getOwner().getEntityCollection(); 435 } 436 437 double x = dataset.getXValue(series, item); 438 double y = dataset.getYValue(series, item); 439 if (Double.isNaN(x) || Double.isNaN(y)) { 440 // can't draw anything 441 return; 442 } 443 444 double transX = domainAxis.valueToJava2D(x, dataArea, 445 plot.getDomainAxisEdge()); 446 double transY = rangeAxis.valueToJava2D(y, dataArea, 447 plot.getRangeAxisEdge()); 448 449 PlotOrientation orientation = plot.getOrientation(); 450 451 // draw optional guide lines 452 if ((pass == 0) && this.guideLinesVisible) { 453 g2.setStroke(this.guideLineStroke); 454 g2.setPaint(this.guideLinePaint); 455 if (orientation == PlotOrientation.HORIZONTAL) { 456 g2.draw(new Line2D.Double(transY, dataArea.getMinY(), transY, 457 dataArea.getMaxY())); 458 g2.draw(new Line2D.Double(dataArea.getMinX(), transX, 459 dataArea.getMaxX(), transX)); 460 } 461 else { 462 g2.draw(new Line2D.Double(transX, dataArea.getMinY(), transX, 463 dataArea.getMaxY())); 464 g2.draw(new Line2D.Double(dataArea.getMinX(), transY, 465 dataArea.getMaxX(), transY)); 466 } 467 } 468 else if (pass == 1) { 469 Shape shape = getItemShape(series, item); 470 if (orientation == PlotOrientation.HORIZONTAL) { 471 shape = ShapeUtilities.createTranslatedShape(shape, transY, 472 transX); 473 } 474 else if (orientation == PlotOrientation.VERTICAL) { 475 shape = ShapeUtilities.createTranslatedShape(shape, transX, 476 transY); 477 } 478 hotspot = shape; 479 if (shape.intersects(dataArea)) { 480 //if (getItemShapeFilled(series, item)) { 481 g2.setPaint(getPaint(dataset, series, item)); 482 g2.fill(shape); 483 //} 484 if (this.drawOutlines) { 485 if (getUseOutlinePaint()) { 486 g2.setPaint(getItemOutlinePaint(series, item)); 487 } 488 else { 489 g2.setPaint(getItemPaint(series, item)); 490 } 491 g2.setStroke(getItemOutlineStroke(series, item)); 492 g2.draw(shape); 493 } 494 } 495 496 // add an entity for the item... 497 if (entities != null) { 498 addEntity(entities, hotspot, dataset, series, item, transX, 499 transY); 500 } 501 } 502 } 503 504 /** 505 * Get the paint for a given series and item from a dataset. 506 * 507 * @param dataset the dataset.. 508 * @param series the series index. 509 * @param item the item index. 510 * 511 * @return The paint. 512 */ 513 protected Paint getPaint(XYDataset dataset, int series, int item) { 514 Paint p = null; 515 if (dataset instanceof XYZDataset) { 516 double z = ((XYZDataset) dataset).getZValue(series, item); 517 p = this.paintScale.getPaint(z); 518 } 519 else { 520 if (this.useFillPaint) { 521 p = getItemFillPaint(series, item); 522 } 523 else { 524 p = getItemPaint(series, item); 525 } 526 } 527 return p; 528 } 529 530 /** 531 * Tests this instance for equality with an arbitrary object. This method 532 * returns <code>true</code> if and only if: 533 * <ul> 534 * <li><code>obj</code> is an instance of <code>XYShapeRenderer</code> (not 535 * <code>null</code>);</li> 536 * <li><code>obj</code> has the same field values as this 537 * <code>XYShapeRenderer</code>;</li> 538 * </ul> 539 * 540 * @param obj the object (<code>null</code> permitted). 541 * 542 * @return A boolean. 543 */ 544 public boolean equals(Object obj) { 545 if (obj == this) { 546 return true; 547 } 548 if (!(obj instanceof XYShapeRenderer)) { 549 return false; 550 } 551 XYShapeRenderer that = (XYShapeRenderer) obj; 552 if (!this.paintScale.equals(that.paintScale)) { 553 return false; 554 } 555 if (this.drawOutlines != that.drawOutlines) { 556 return false; 557 } 558 if (this.useOutlinePaint != that.useOutlinePaint) { 559 return false; 560 } 561 if (this.useFillPaint != that.useFillPaint) { 562 return false; 563 } 564 if (this.guideLinesVisible != that.guideLinesVisible) { 565 return false; 566 } 567 if (!this.guideLinePaint.equals(that.guideLinePaint)) { 568 return false; 569 } 570 if (!this.guideLineStroke.equals(that.guideLineStroke)) { 571 return false; 572 } 573 return super.equals(obj); 574 } 575 576 /** 577 * Returns a clone of this renderer. 578 * 579 * @return A clone of this renderer. 580 * 581 * @throws CloneNotSupportedException if there is a problem creating the 582 * clone. 583 */ 584 public Object clone() throws CloneNotSupportedException { 585 XYShapeRenderer clone = (XYShapeRenderer) super.clone(); 586 if (this.paintScale instanceof PublicCloneable) { 587 PublicCloneable pc = (PublicCloneable) this.paintScale; 588 clone.paintScale = (PaintScale) pc.clone(); 589 } 590 return clone; 591 } 592 593 /** 594 * Provides serialization support. 595 * 596 * @param stream the input stream. 597 * 598 * @throws IOException if there is an I/O error. 599 * @throws ClassNotFoundException if there is a classpath problem. 600 */ 601 private void readObject(ObjectInputStream stream) 602 throws IOException, ClassNotFoundException { 603 stream.defaultReadObject(); 604 this.guideLinePaint = SerialUtilities.readPaint(stream); 605 this.guideLineStroke = SerialUtilities.readStroke(stream); 606 } 607 608 /** 609 * Provides serialization support. 610 * 611 * @param stream the output stream. 612 * 613 * @throws IOException if there is an I/O error. 614 */ 615 private void writeObject(ObjectOutputStream stream) throws IOException { 616 stream.defaultWriteObject(); 617 SerialUtilities.writePaint(this.guideLinePaint, stream); 618 SerialUtilities.writeStroke(this.guideLineStroke, stream); 619 } 620 621}