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