001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, 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-2020 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 */ 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 * 088 * @since 1.0.11 089 */ 090public class XYShapeRenderer extends AbstractXYItemRenderer 091 implements XYItemRenderer, Cloneable, Serializable { 092 093 /** Auto generated serial version id. */ 094 private static final long serialVersionUID = 8320552104211173221L; 095 096 /** The paint scale (never null). */ 097 private PaintScale paintScale; 098 099 /** A flag that controls whether or not the shape outlines are drawn. */ 100 private boolean drawOutlines; 101 102 /** 103 * A flag that controls whether or not the outline paint is used (if not, 104 * the regular paint is used). 105 */ 106 private boolean useOutlinePaint; 107 108 /** 109 * A flag that controls whether or not the fill paint is used (if not, 110 * the fill paint is used). 111 */ 112 private boolean useFillPaint; 113 114 /** Flag indicating if guide lines should be drawn for every item. */ 115 private boolean guideLinesVisible; 116 117 /** The paint used for drawing the guide lines (never null). */ 118 private transient Paint guideLinePaint; 119 120 /** The stroke used for drawing the guide lines (never null). */ 121 private transient Stroke guideLineStroke; 122 123 /** 124 * Creates a new {@code XYShapeRenderer} instance with default 125 * attributes. 126 */ 127 public XYShapeRenderer() { 128 this.paintScale = new LookupPaintScale(); 129 this.useFillPaint = false; 130 this.drawOutlines = false; 131 this.useOutlinePaint = true; 132 this.guideLinesVisible = false; 133 this.guideLinePaint = Color.darkGray; 134 this.guideLineStroke = new BasicStroke(); 135 setDefaultShape(new Ellipse2D.Double(-5.0, -5.0, 10.0, 10.0)); 136 setAutoPopulateSeriesShape(false); 137 } 138 139 /** 140 * Returns the paint scale used by the renderer. 141 * 142 * @return The paint scale (never {@code null}). 143 * 144 * @see #setPaintScale(PaintScale) 145 */ 146 public PaintScale getPaintScale() { 147 return this.paintScale; 148 } 149 150 /** 151 * Sets the paint scale used by the renderer and sends a 152 * {@link RendererChangeEvent} to all registered listeners. 153 * 154 * @param scale the scale ({@code null} not permitted). 155 * 156 * @see #getPaintScale() 157 */ 158 public void setPaintScale(PaintScale scale) { 159 Args.nullNotPermitted(scale, "scale"); 160 this.paintScale = scale; 161 notifyListeners(new RendererChangeEvent(this)); 162 } 163 164 /** 165 * Returns {@code true} if outlines should be drawn for shapes, and 166 * {@code false} 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} if the renderer should use the fill paint 195 * setting to fill shapes, and {@code false} if it should just 196 * use the regular paint. 197 * <p> 198 * Refer to {@code XYLineAndShapeRendererDemo2.java} 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}). 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} not permitted). 293 * 294 * @see #getGuideLinePaint() 295 */ 296 public void setGuideLinePaint(Paint paint) { 297 Args.nullNotPermitted(paint, "paint"); 298 this.guideLinePaint = paint; 299 fireChangeEvent(); 300 } 301 302 /** 303 * Returns the stroke used to draw the guide lines. 304 * 305 * @return The stroke. 306 * 307 * @see #setGuideLineStroke(Stroke) 308 */ 309 public Stroke getGuideLineStroke() { 310 return this.guideLineStroke; 311 } 312 313 /** 314 * Sets the stroke used to draw the guide lines and sends a 315 * {@link RendererChangeEvent} to all registered listeners. 316 * 317 * @param stroke the stroke ({@code null} not permitted). 318 * 319 * @see #getGuideLineStroke() 320 */ 321 public void setGuideLineStroke(Stroke stroke) { 322 Args.nullNotPermitted(stroke, "stroke"); 323 this.guideLineStroke = stroke; 324 fireChangeEvent(); 325 } 326 327 /** 328 * Returns the lower and upper bounds (range) of the x-values in the 329 * specified dataset. 330 * 331 * @param dataset the dataset ({@code null} permitted). 332 * 333 * @return The range ({@code null} if the dataset is {@code null} 334 * or empty). 335 */ 336 @Override 337 public Range findDomainBounds(XYDataset dataset) { 338 if (dataset == null) { 339 return null; 340 } 341 Range r = DatasetUtils.findDomainBounds(dataset, false); 342 if (r == null) { 343 return null; 344 } 345 double offset = 0; // TODO getSeriesShape(n).getBounds().width / 2; 346 return new Range(r.getLowerBound() + offset, 347 r.getUpperBound() + offset); 348 } 349 350 /** 351 * Returns the range of values the renderer requires to display all the 352 * items from the specified dataset. 353 * 354 * @param dataset the dataset ({@code null} permitted). 355 * 356 * @return The range ({@code null} if the dataset is {@code null} 357 * or empty). 358 */ 359 @Override 360 public Range findRangeBounds(XYDataset dataset) { 361 if (dataset == null) { 362 return null; 363 } 364 Range r = DatasetUtils.findRangeBounds(dataset, false); 365 if (r == null) { 366 return null; 367 } 368 double offset = 0; // TODO getSeriesShape(n).getBounds().height / 2; 369 return new Range(r.getLowerBound() + offset, r.getUpperBound() 370 + offset); 371 } 372 373 /** 374 * Return the range of z-values in the specified dataset. 375 * 376 * @param dataset the dataset ({@code null} permitted). 377 * 378 * @return The range ({@code null} if the dataset is {@code null} 379 * or empty). 380 */ 381 public Range findZBounds(XYZDataset dataset) { 382 if (dataset != null) { 383 return DatasetUtils.findZBounds(dataset); 384 } else { 385 return null; 386 } 387 } 388 389 /** 390 * Returns the number of passes required by this renderer. 391 * 392 * @return {@code 2}. 393 */ 394 @Override 395 public int getPassCount() { 396 return 2; 397 } 398 399 /** 400 * Draws the block representing the specified item. 401 * 402 * @param g2 the graphics device. 403 * @param state the state. 404 * @param dataArea the data area. 405 * @param info the plot rendering info. 406 * @param plot the plot. 407 * @param domainAxis the x-axis. 408 * @param rangeAxis the y-axis. 409 * @param dataset the dataset. 410 * @param series the series index. 411 * @param item the item index. 412 * @param crosshairState the crosshair state. 413 * @param pass the pass index. 414 */ 415 @Override 416 public void drawItem(Graphics2D g2, XYItemRendererState state, 417 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 418 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 419 int series, int item, CrosshairState crosshairState, int pass) { 420 421 Shape hotspot; 422 EntityCollection entities = null; 423 if (info != null) { 424 entities = info.getOwner().getEntityCollection(); 425 } 426 427 double x = dataset.getXValue(series, item); 428 double y = dataset.getYValue(series, item); 429 if (Double.isNaN(x) || Double.isNaN(y)) { 430 // can't draw anything 431 return; 432 } 433 434 double transX = domainAxis.valueToJava2D(x, dataArea, 435 plot.getDomainAxisEdge()); 436 double transY = rangeAxis.valueToJava2D(y, dataArea, 437 plot.getRangeAxisEdge()); 438 439 PlotOrientation orientation = plot.getOrientation(); 440 441 // draw optional guide lines 442 if ((pass == 0) && this.guideLinesVisible) { 443 g2.setStroke(this.guideLineStroke); 444 g2.setPaint(this.guideLinePaint); 445 if (orientation == PlotOrientation.HORIZONTAL) { 446 g2.draw(new Line2D.Double(transY, dataArea.getMinY(), transY, 447 dataArea.getMaxY())); 448 g2.draw(new Line2D.Double(dataArea.getMinX(), transX, 449 dataArea.getMaxX(), transX)); 450 } else { 451 g2.draw(new Line2D.Double(transX, dataArea.getMinY(), transX, 452 dataArea.getMaxY())); 453 g2.draw(new Line2D.Double(dataArea.getMinX(), transY, 454 dataArea.getMaxX(), transY)); 455 } 456 } else if (pass == 1) { 457 Shape shape = getItemShape(series, item); 458 if (orientation == PlotOrientation.HORIZONTAL) { 459 shape = ShapeUtils.createTranslatedShape(shape, transY, 460 transX); 461 } else if (orientation == PlotOrientation.VERTICAL) { 462 shape = ShapeUtils.createTranslatedShape(shape, transX, 463 transY); 464 } 465 hotspot = shape; 466 if (shape.intersects(dataArea)) { 467 //if (getItemShapeFilled(series, item)) { 468 g2.setPaint(getPaint(dataset, series, item)); 469 g2.fill(shape); 470 //} 471 if (this.drawOutlines) { 472 if (getUseOutlinePaint()) { 473 g2.setPaint(getItemOutlinePaint(series, item)); 474 } else { 475 g2.setPaint(getItemPaint(series, item)); 476 } 477 g2.setStroke(getItemOutlineStroke(series, item)); 478 g2.draw(shape); 479 } 480 } 481 482 int datasetIndex = plot.indexOf(dataset); 483 updateCrosshairValues(crosshairState, x, y, datasetIndex, 484 transX, transY, orientation); 485 486 // add an entity for the item... 487 if (entities != null) { 488 addEntity(entities, hotspot, dataset, series, item, 0.0, 0.0); 489 } 490 } 491 } 492 493 /** 494 * Get the paint for a given series and item from a dataset. 495 * 496 * @param dataset the dataset. 497 * @param series the series index. 498 * @param item the item index. 499 * 500 * @return The paint. 501 */ 502 protected Paint getPaint(XYDataset dataset, int series, int item) { 503 Paint p; 504 if (dataset instanceof XYZDataset) { 505 double z = ((XYZDataset) dataset).getZValue(series, item); 506 p = this.paintScale.getPaint(z); 507 } else { 508 if (this.useFillPaint) { 509 p = getItemFillPaint(series, item); 510 } 511 else { 512 p = getItemPaint(series, item); 513 } 514 } 515 return p; 516 } 517 518 /** 519 * Tests this instance for equality with an arbitrary object. This method 520 * returns {@code true} if and only if: 521 * <ul> 522 * <li>{@code obj} is an instance of {@code XYShapeRenderer} (not 523 * {@code null});</li> 524 * <li>{@code obj} has the same field values as this 525 * {@code XYShapeRenderer};</li> 526 * </ul> 527 * 528 * @param obj the object ({@code null} permitted). 529 * 530 * @return A boolean. 531 */ 532 @Override 533 public boolean equals(Object obj) { 534 if (obj == this) { 535 return true; 536 } 537 if (!(obj instanceof XYShapeRenderer)) { 538 return false; 539 } 540 XYShapeRenderer that = (XYShapeRenderer) obj; 541 if (!this.paintScale.equals(that.paintScale)) { 542 return false; 543 } 544 if (this.drawOutlines != that.drawOutlines) { 545 return false; 546 } 547 if (this.useOutlinePaint != that.useOutlinePaint) { 548 return false; 549 } 550 if (this.useFillPaint != that.useFillPaint) { 551 return false; 552 } 553 if (this.guideLinesVisible != that.guideLinesVisible) { 554 return false; 555 } 556 if (!this.guideLinePaint.equals(that.guideLinePaint)) { 557 return false; 558 } 559 if (!this.guideLineStroke.equals(that.guideLineStroke)) { 560 return false; 561 } 562 return super.equals(obj); 563 } 564 565 /** 566 * Returns a clone of this renderer. 567 * 568 * @return A clone of this renderer. 569 * 570 * @throws CloneNotSupportedException if there is a problem creating the 571 * clone. 572 */ 573 @Override 574 public Object clone() throws CloneNotSupportedException { 575 XYShapeRenderer clone = (XYShapeRenderer) super.clone(); 576 if (this.paintScale instanceof PublicCloneable) { 577 PublicCloneable pc = (PublicCloneable) this.paintScale; 578 clone.paintScale = (PaintScale) pc.clone(); 579 } 580 return clone; 581 } 582 583 /** 584 * Provides serialization support. 585 * 586 * @param stream the input stream. 587 * 588 * @throws IOException if there is an I/O error. 589 * @throws ClassNotFoundException if there is a classpath problem. 590 */ 591 private void readObject(ObjectInputStream stream) 592 throws IOException, ClassNotFoundException { 593 stream.defaultReadObject(); 594 this.guideLinePaint = SerialUtils.readPaint(stream); 595 this.guideLineStroke = SerialUtils.readStroke(stream); 596 } 597 598 /** 599 * Provides serialization support. 600 * 601 * @param stream the output stream. 602 * 603 * @throws IOException if there is an I/O error. 604 */ 605 private void writeObject(ObjectOutputStream stream) throws IOException { 606 stream.defaultWriteObject(); 607 SerialUtils.writePaint(this.guideLinePaint, stream); 608 SerialUtils.writeStroke(this.guideLineStroke, stream); 609 } 610 611}