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 * XYShapeRenderer.java 029 * -------------------- 030 * (C) Copyright 2008-2014 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 * 03-Jul-2013 : Use ParamChecks (DG); 043 * 044 */ 045 046package org.jfree.chart.renderer.xy; 047 048import java.awt.BasicStroke; 049import java.awt.Color; 050import java.awt.Graphics2D; 051import java.awt.Paint; 052import java.awt.Shape; 053import java.awt.Stroke; 054import java.awt.geom.Ellipse2D; 055import java.awt.geom.Line2D; 056import java.awt.geom.Rectangle2D; 057import java.io.IOException; 058import java.io.ObjectInputStream; 059import java.io.ObjectOutputStream; 060import java.io.Serializable; 061 062import org.jfree.chart.axis.ValueAxis; 063import org.jfree.chart.entity.EntityCollection; 064import org.jfree.chart.event.RendererChangeEvent; 065import org.jfree.chart.plot.CrosshairState; 066import org.jfree.chart.plot.PlotOrientation; 067import org.jfree.chart.plot.PlotRenderingInfo; 068import org.jfree.chart.plot.XYPlot; 069import org.jfree.chart.renderer.LookupPaintScale; 070import org.jfree.chart.renderer.PaintScale; 071import org.jfree.chart.util.ParamChecks; 072import org.jfree.data.Range; 073import org.jfree.data.general.DatasetUtilities; 074import org.jfree.data.xy.XYDataset; 075import org.jfree.data.xy.XYZDataset; 076import org.jfree.io.SerialUtilities; 077import org.jfree.util.PublicCloneable; 078import org.jfree.util.ShapeUtilities; 079 080/** 081 * A renderer that draws shapes at (x, y) coordinates and, if the dataset 082 * is an instance of {@link XYZDataset}, fills the shapes with a paint that 083 * is based on the z-value (the paint is obtained from a lookup table). The 084 * renderer also allows for optional guidelines, horizontal and vertical lines 085 * connecting the shape to the edges of the plot. 086 * <br><br> 087 * The example shown here is generated by the 088 * <code>XYShapeRendererDemo1.java</code> program included in the JFreeChart 089 * demo collection: 090 * <br><br> 091 * <img src="../../../../../images/XYShapeRendererSample.png" 092 * alt="XYShapeRendererSample.png"> 093 * <br><br> 094 * This renderer has similarities to, but also differences from, the 095 * {@link XYLineAndShapeRenderer}. 096 * 097 * @since 1.0.11 098 */ 099public class XYShapeRenderer extends AbstractXYItemRenderer 100 implements XYItemRenderer, Cloneable, Serializable { 101 102 /** Auto generated serial version id. */ 103 private static final long serialVersionUID = 8320552104211173221L; 104 105 /** The paint scale (never null). */ 106 private PaintScale paintScale; 107 108 /** A flag that controls whether or not the shape outlines are drawn. */ 109 private boolean drawOutlines; 110 111 /** 112 * A flag that controls whether or not the outline paint is used (if not, 113 * the regular paint is used). 114 */ 115 private boolean useOutlinePaint; 116 117 /** 118 * A flag that controls whether or not the fill paint is used (if not, 119 * the fill paint is used). 120 */ 121 private boolean useFillPaint; 122 123 /** Flag indicating if guide lines should be drawn for every item. */ 124 private boolean guideLinesVisible; 125 126 /** The paint used for drawing the guide lines (never null). */ 127 private transient Paint guideLinePaint; 128 129 /** The stroke used for drawing the guide lines (never null). */ 130 private transient Stroke guideLineStroke; 131 132 /** 133 * Creates a new <code>XYShapeRenderer</code> instance with default 134 * attributes. 135 */ 136 public XYShapeRenderer() { 137 this.paintScale = new LookupPaintScale(); 138 this.useFillPaint = false; 139 this.drawOutlines = false; 140 this.useOutlinePaint = true; 141 this.guideLinesVisible = false; 142 this.guideLinePaint = Color.darkGray; 143 this.guideLineStroke = new BasicStroke(); 144 setBaseShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0)); 145 setAutoPopulateSeriesShape(false); 146 } 147 148 /** 149 * Returns the paint scale used by the renderer. 150 * 151 * @return The paint scale (never <code>null</code>). 152 * 153 * @see #setPaintScale(PaintScale) 154 */ 155 public PaintScale getPaintScale() { 156 return this.paintScale; 157 } 158 159 /** 160 * Sets the paint scale used by the renderer and sends a 161 * {@link RendererChangeEvent} to all registered listeners. 162 * 163 * @param scale the scale (<code>null</code> not permitted). 164 * 165 * @see #getPaintScale() 166 */ 167 public void setPaintScale(PaintScale scale) { 168 ParamChecks.nullNotPermitted(scale, "scale"); 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 ParamChecks.nullNotPermitted(paint, "paint"); 307 this.guideLinePaint = paint; 308 fireChangeEvent(); 309 } 310 311 /** 312 * Returns the stroke used to draw the guide lines. 313 * 314 * @return The stroke. 315 * 316 * @see #setGuideLineStroke(Stroke) 317 */ 318 public Stroke getGuideLineStroke() { 319 return this.guideLineStroke; 320 } 321 322 /** 323 * Sets the stroke used to draw the guide lines and sends a 324 * {@link RendererChangeEvent} to all registered listeners. 325 * 326 * @param stroke the stroke (<code>null</code> not permitted). 327 * 328 * @see #getGuideLineStroke() 329 */ 330 public void setGuideLineStroke(Stroke stroke) { 331 ParamChecks.nullNotPermitted(stroke, "stroke"); 332 this.guideLineStroke = stroke; 333 fireChangeEvent(); 334 } 335 336 /** 337 * Returns the lower and upper bounds (range) of the x-values in the 338 * specified dataset. 339 * 340 * @param dataset the dataset (<code>null</code> permitted). 341 * 342 * @return The range (<code>null</code> if the dataset is <code>null</code> 343 * or empty). 344 */ 345 @Override 346 public Range findDomainBounds(XYDataset dataset) { 347 if (dataset == null) { 348 return null; 349 } 350 Range r = DatasetUtilities.findDomainBounds(dataset, false); 351 if (r == null) { 352 return null; 353 } 354 double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2; 355 return new Range(r.getLowerBound() + offset, 356 r.getUpperBound() + offset); 357 } 358 359 /** 360 * Returns the range of values the renderer requires to display all the 361 * items from the specified dataset. 362 * 363 * @param dataset the dataset (<code>null</code> permitted). 364 * 365 * @return The range (<code>null</code> if the dataset is <code>null</code> 366 * or empty). 367 */ 368 @Override 369 public Range findRangeBounds(XYDataset dataset) { 370 if (dataset == null) { 371 return null; 372 } 373 Range r = DatasetUtilities.findRangeBounds(dataset, false); 374 if (r == null) { 375 return null; 376 } 377 double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2; 378 return new Range(r.getLowerBound() + offset, r.getUpperBound() 379 + offset); 380 } 381 382 /** 383 * Return the range of z-values in the specified dataset. 384 * 385 * @param dataset the dataset (<code>null</code> permitted). 386 * 387 * @return The range (<code>null</code> if the dataset is <code>null</code> 388 * or empty). 389 */ 390 public Range findZBounds(XYZDataset dataset) { 391 if (dataset != null) { 392 return DatasetUtilities.findZBounds(dataset); 393 } 394 else { 395 return null; 396 } 397 } 398 399 /** 400 * Returns the number of passes required by this renderer. 401 * 402 * @return <code>2</code>. 403 */ 404 @Override 405 public int getPassCount() { 406 return 2; 407 } 408 409 /** 410 * Draws the block representing the specified item. 411 * 412 * @param g2 the graphics device. 413 * @param state the state. 414 * @param dataArea the data area. 415 * @param info the plot rendering info. 416 * @param plot the plot. 417 * @param domainAxis the x-axis. 418 * @param rangeAxis the y-axis. 419 * @param dataset the dataset. 420 * @param series the series index. 421 * @param item the item index. 422 * @param crosshairState the crosshair state. 423 * @param pass the pass index. 424 */ 425 @Override 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; 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; 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 @Override 545 public boolean equals(Object obj) { 546 if (obj == this) { 547 return true; 548 } 549 if (!(obj instanceof XYShapeRenderer)) { 550 return false; 551 } 552 XYShapeRenderer that = (XYShapeRenderer) obj; 553 if (!this.paintScale.equals(that.paintScale)) { 554 return false; 555 } 556 if (this.drawOutlines != that.drawOutlines) { 557 return false; 558 } 559 if (this.useOutlinePaint != that.useOutlinePaint) { 560 return false; 561 } 562 if (this.useFillPaint != that.useFillPaint) { 563 return false; 564 } 565 if (this.guideLinesVisible != that.guideLinesVisible) { 566 return false; 567 } 568 if (!this.guideLinePaint.equals(that.guideLinePaint)) { 569 return false; 570 } 571 if (!this.guideLineStroke.equals(that.guideLineStroke)) { 572 return false; 573 } 574 return super.equals(obj); 575 } 576 577 /** 578 * Returns a clone of this renderer. 579 * 580 * @return A clone of this renderer. 581 * 582 * @throws CloneNotSupportedException if there is a problem creating the 583 * clone. 584 */ 585 @Override 586 public Object clone() throws CloneNotSupportedException { 587 XYShapeRenderer clone = (XYShapeRenderer) super.clone(); 588 if (this.paintScale instanceof PublicCloneable) { 589 PublicCloneable pc = (PublicCloneable) this.paintScale; 590 clone.paintScale = (PaintScale) pc.clone(); 591 } 592 return clone; 593 } 594 595 /** 596 * Provides serialization support. 597 * 598 * @param stream the input stream. 599 * 600 * @throws IOException if there is an I/O error. 601 * @throws ClassNotFoundException if there is a classpath problem. 602 */ 603 private void readObject(ObjectInputStream stream) 604 throws IOException, ClassNotFoundException { 605 stream.defaultReadObject(); 606 this.guideLinePaint = SerialUtilities.readPaint(stream); 607 this.guideLineStroke = SerialUtilities.readStroke(stream); 608 } 609 610 /** 611 * Provides serialization support. 612 * 613 * @param stream the output stream. 614 * 615 * @throws IOException if there is an I/O error. 616 */ 617 private void writeObject(ObjectOutputStream stream) throws IOException { 618 stream.defaultWriteObject(); 619 SerialUtilities.writePaint(this.guideLinePaint, stream); 620 SerialUtilities.writeStroke(this.guideLineStroke, stream); 621 } 622 623}