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