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