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