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 * XYBlockRenderer.java 029 * -------------------- 030 * (C) Copyright 2006-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): -; 034 * 035 */ 036 037package org.jfree.chart.renderer.xy; 038 039import java.awt.Graphics2D; 040import java.awt.Paint; 041import java.awt.geom.Rectangle2D; 042import java.io.Serializable; 043 044import org.jfree.chart.axis.ValueAxis; 045import org.jfree.chart.entity.EntityCollection; 046import org.jfree.chart.event.RendererChangeEvent; 047import org.jfree.chart.plot.CrosshairState; 048import org.jfree.chart.plot.PlotOrientation; 049import org.jfree.chart.plot.PlotRenderingInfo; 050import org.jfree.chart.plot.XYPlot; 051import org.jfree.chart.renderer.LookupPaintScale; 052import org.jfree.chart.renderer.PaintScale; 053import org.jfree.chart.api.RectangleAnchor; 054import org.jfree.chart.internal.Args; 055import org.jfree.chart.api.PublicCloneable; 056import org.jfree.data.Range; 057import org.jfree.data.general.DatasetUtils; 058import org.jfree.data.xy.XYDataset; 059import org.jfree.data.xy.XYZDataset; 060 061/** 062 * A renderer that represents data from an {@link XYZDataset} by drawing a 063 * color block at each (x, y) point, where the color is a function of the 064 * z-value from the dataset. The example shown here is generated by the 065 * {@code XYBlockChartDemo1.java} program included in the JFreeChart 066 * demo collection: 067 * <br><br> 068 * <img src="doc-files/XYBlockRendererSample.png" alt="XYBlockRendererSample.png"> 069 */ 070public class XYBlockRenderer extends AbstractXYItemRenderer 071 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 072 073 /** 074 * The block width (defaults to 1.0). 075 */ 076 private double blockWidth = 1.0; 077 078 /** 079 * The block height (defaults to 1.0). 080 */ 081 private double blockHeight = 1.0; 082 083 /** 084 * The anchor point used to align each block to its (x, y) location. The 085 * default value is {@code RectangleAnchor.CENTER}. 086 */ 087 private RectangleAnchor blockAnchor = RectangleAnchor.CENTER; 088 089 /** Temporary storage for the x-offset used to align the block anchor. */ 090 private double xOffset; 091 092 /** Temporary storage for the y-offset used to align the block anchor. */ 093 private double yOffset; 094 095 /** The paint scale. */ 096 private PaintScale paintScale; 097 098 /** A flag that controls whether outlines are drawn for blocks. */ 099 private boolean drawOutlines; 100 101 /** 102 * A flag that controls whether the outline paint is used for drawing block 103 * outlines. 104 */ 105 private boolean useOutlinePaint; 106 107 /** 108 * Creates a new {@code XYBlockRenderer} instance with default 109 * attributes. 110 */ 111 public XYBlockRenderer() { 112 updateOffsets(); 113 this.paintScale = new LookupPaintScale(); 114 this.drawOutlines = true; 115 this.useOutlinePaint = false; // use item paint by default 116 } 117 118 /** 119 * Returns the block width, in data/axis units. 120 * 121 * @return The block width. 122 * 123 * @see #setBlockWidth(double) 124 */ 125 public double getBlockWidth() { 126 return this.blockWidth; 127 } 128 129 /** 130 * Sets the width of the blocks used to represent each data item and 131 * sends a {@link RendererChangeEvent} to all registered listeners. 132 * 133 * @param width the new width, in data/axis units (must be > 0.0). 134 * 135 * @see #getBlockWidth() 136 */ 137 public void setBlockWidth(double width) { 138 if (width <= 0.0) { 139 throw new IllegalArgumentException( 140 "The 'width' argument must be > 0.0"); 141 } 142 this.blockWidth = width; 143 updateOffsets(); 144 fireChangeEvent(); 145 } 146 147 /** 148 * Returns the block height, in data/axis units. 149 * 150 * @return The block height. 151 * 152 * @see #setBlockHeight(double) 153 */ 154 public double getBlockHeight() { 155 return this.blockHeight; 156 } 157 158 /** 159 * Sets the height of the blocks used to represent each data item and 160 * sends a {@link RendererChangeEvent} to all registered listeners. 161 * 162 * @param height the new height, in data/axis units (must be > 0.0). 163 * 164 * @see #getBlockHeight() 165 */ 166 public void setBlockHeight(double height) { 167 if (height <= 0.0) { 168 throw new IllegalArgumentException( 169 "The 'height' argument must be > 0.0"); 170 } 171 this.blockHeight = height; 172 updateOffsets(); 173 fireChangeEvent(); 174 } 175 176 /** 177 * Returns the anchor point used to align a block at its (x, y) location. 178 * The default values is {@link RectangleAnchor#CENTER}. 179 * 180 * @return The anchor point (never {@code null}). 181 * 182 * @see #setBlockAnchor(RectangleAnchor) 183 */ 184 public RectangleAnchor getBlockAnchor() { 185 return this.blockAnchor; 186 } 187 188 /** 189 * Sets the anchor point used to align a block at its (x, y) location and 190 * sends a {@link RendererChangeEvent} to all registered listeners. 191 * 192 * @param anchor the anchor. 193 * 194 * @see #getBlockAnchor() 195 */ 196 public void setBlockAnchor(RectangleAnchor anchor) { 197 Args.nullNotPermitted(anchor, "anchor"); 198 if (this.blockAnchor.equals(anchor)) { 199 return; // no change 200 } 201 this.blockAnchor = anchor; 202 updateOffsets(); 203 fireChangeEvent(); 204 } 205 206 /** 207 * Returns the paint scale used by the renderer. 208 * 209 * @return The paint scale (never {@code null}). 210 * 211 * @see #setPaintScale(PaintScale) 212 */ 213 public PaintScale getPaintScale() { 214 return this.paintScale; 215 } 216 217 /** 218 * Sets the paint scale used by the renderer and sends a 219 * {@link RendererChangeEvent} to all registered listeners. 220 * 221 * @param scale the scale ({@code null} not permitted). 222 * 223 * @see #getPaintScale() 224 */ 225 public void setPaintScale(PaintScale scale) { 226 Args.nullNotPermitted(scale, "scale"); 227 this.paintScale = scale; 228 fireChangeEvent(); 229 } 230 231 /** 232 * Returns {@code true} if outlines should be drawn for blocks, and 233 * {@code false} otherwise. The default value is {@code true}. 234 * 235 * @return A boolean. 236 * 237 * @see #setDrawOutlines(boolean) 238 */ 239 public boolean getDrawOutlines() { 240 return this.drawOutlines; 241 } 242 243 /** 244 * Sets the flag that controls whether outlines are drawn for 245 * blocks, and sends a {@link RendererChangeEvent} to all registered 246 * listeners. 247 * 248 * @param flag the flag. 249 * 250 * @see #getDrawOutlines() 251 */ 252 public void setDrawOutlines(boolean flag) { 253 this.drawOutlines = flag; 254 fireChangeEvent(); 255 } 256 257 /** 258 * Returns {@code true} if the renderer should use the outline paint 259 * setting to draw block outlines, and {@code false} if it should just 260 * use the regular item paint. 261 * 262 * @return A boolean. 263 * 264 * @see #setUseOutlinePaint(boolean) 265 */ 266 public boolean getUseOutlinePaint() { 267 return this.useOutlinePaint; 268 } 269 270 /** 271 * Sets the flag that controls whether the outline paint is used to draw 272 * block outlines, and sends a {@link RendererChangeEvent} to all 273 * registered listeners. 274 * 275 * @param flag the flag. 276 * 277 * @see #getUseOutlinePaint() 278 */ 279 public void setUseOutlinePaint(boolean flag) { 280 this.useOutlinePaint = flag; 281 fireChangeEvent(); 282 } 283 284 /** 285 * Updates the offsets to take into account the block width, height and 286 * anchor. 287 */ 288 private void updateOffsets() { 289 if (this.blockAnchor.equals(RectangleAnchor.BOTTOM_LEFT)) { 290 this.xOffset = 0.0; 291 this.yOffset = 0.0; 292 } 293 else if (this.blockAnchor.equals(RectangleAnchor.BOTTOM)) { 294 this.xOffset = -this.blockWidth / 2.0; 295 this.yOffset = 0.0; 296 } 297 else if (this.blockAnchor.equals(RectangleAnchor.BOTTOM_RIGHT)) { 298 this.xOffset = -this.blockWidth; 299 this.yOffset = 0.0; 300 } 301 else if (this.blockAnchor.equals(RectangleAnchor.LEFT)) { 302 this.xOffset = 0.0; 303 this.yOffset = -this.blockHeight / 2.0; 304 } 305 else if (this.blockAnchor.equals(RectangleAnchor.CENTER)) { 306 this.xOffset = -this.blockWidth / 2.0; 307 this.yOffset = -this.blockHeight / 2.0; 308 } 309 else if (this.blockAnchor.equals(RectangleAnchor.RIGHT)) { 310 this.xOffset = -this.blockWidth; 311 this.yOffset = -this.blockHeight / 2.0; 312 } 313 else if (this.blockAnchor.equals(RectangleAnchor.TOP_LEFT)) { 314 this.xOffset = 0.0; 315 this.yOffset = -this.blockHeight; 316 } 317 else if (this.blockAnchor.equals(RectangleAnchor.TOP)) { 318 this.xOffset = -this.blockWidth / 2.0; 319 this.yOffset = -this.blockHeight; 320 } 321 else if (this.blockAnchor.equals(RectangleAnchor.TOP_RIGHT)) { 322 this.xOffset = -this.blockWidth; 323 this.yOffset = -this.blockHeight; 324 } 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 * @see #findRangeBounds(XYDataset) 337 */ 338 @Override 339 public Range findDomainBounds(XYDataset dataset) { 340 if (dataset == null) { 341 return null; 342 } 343 Range r = DatasetUtils.findDomainBounds(dataset, false); 344 if (r == null) { 345 return null; 346 } 347 return new Range(r.getLowerBound() + this.xOffset, 348 r.getUpperBound() + this.blockWidth + this.xOffset); 349 } 350 351 /** 352 * Returns the range of values the renderer requires to display all the 353 * items from the specified dataset. 354 * 355 * @param dataset the dataset ({@code null} permitted). 356 * 357 * @return The range ({@code null} if the dataset is {@code null} 358 * or empty). 359 * 360 * @see #findDomainBounds(XYDataset) 361 */ 362 @Override 363 public Range findRangeBounds(XYDataset dataset) { 364 if (dataset != null) { 365 Range r = DatasetUtils.findRangeBounds(dataset, false); 366 if (r == null) { 367 return null; 368 } 369 else { 370 return new Range(r.getLowerBound() + this.yOffset, 371 r.getUpperBound() + this.blockHeight + this.yOffset); 372 } 373 } 374 else { 375 return null; 376 } 377 } 378 379 /** 380 * Draws the block representing the specified item. 381 * 382 * @param g2 the graphics device. 383 * @param state the state. 384 * @param dataArea the data area. 385 * @param info the plot rendering info. 386 * @param plot the plot. 387 * @param domainAxis the x-axis. 388 * @param rangeAxis the y-axis. 389 * @param dataset the dataset. 390 * @param series the series index. 391 * @param item the item index. 392 * @param crosshairState the crosshair state. 393 * @param pass the pass index. 394 */ 395 @Override 396 public void drawItem(Graphics2D g2, XYItemRendererState state, 397 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 398 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 399 int series, int item, CrosshairState crosshairState, int pass) { 400 401 double x = dataset.getXValue(series, item); 402 double y = dataset.getYValue(series, item); 403 double z = 0.0; 404 if (dataset instanceof XYZDataset) { 405 z = ((XYZDataset) dataset).getZValue(series, item); 406 } 407 408 Paint p = this.paintScale.getPaint(z); 409 double xx0 = domainAxis.valueToJava2D(x + this.xOffset, dataArea, 410 plot.getDomainAxisEdge()); 411 double yy0 = rangeAxis.valueToJava2D(y + this.yOffset, dataArea, 412 plot.getRangeAxisEdge()); 413 double xx1 = domainAxis.valueToJava2D(x + this.blockWidth 414 + this.xOffset, dataArea, plot.getDomainAxisEdge()); 415 double yy1 = rangeAxis.valueToJava2D(y + this.blockHeight 416 + this.yOffset, dataArea, plot.getRangeAxisEdge()); 417 Rectangle2D block; 418 PlotOrientation orientation = plot.getOrientation(); 419 if (orientation.equals(PlotOrientation.HORIZONTAL)) { 420 block = new Rectangle2D.Double(Math.min(yy0, yy1), 421 Math.min(xx0, xx1), Math.abs(yy1 - yy0), 422 Math.abs(xx0 - xx1)); 423 } 424 else { 425 block = new Rectangle2D.Double(Math.min(xx0, xx1), 426 Math.min(yy0, yy1), Math.abs(xx1 - xx0), 427 Math.abs(yy1 - yy0)); 428 } 429 g2.setPaint(p); 430 g2.fill(block); 431 if (getDrawOutlines()) { 432 if (getUseOutlinePaint()) { 433 g2.setPaint(getItemOutlinePaint(series, item)); 434 } 435 g2.setStroke(lookupSeriesOutlineStroke(series)); 436 g2.draw(block); 437 } 438 439 if (isItemLabelVisible(series, item)) { 440 drawItemLabel(g2, orientation, dataset, series, item, 441 block.getCenterX(), block.getCenterY(), y < 0.0); 442 } 443 444 int datasetIndex = plot.indexOf(dataset); 445 double transX = domainAxis.valueToJava2D(x, dataArea, 446 plot.getDomainAxisEdge()); 447 double transY = rangeAxis.valueToJava2D(y, dataArea, 448 plot.getRangeAxisEdge()); 449 updateCrosshairValues(crosshairState, x, y, datasetIndex, 450 transX, transY, orientation); 451 452 EntityCollection entities = state.getEntityCollection(); 453 if (entities != null) { 454 addEntity(entities, block, dataset, series, item, 455 block.getCenterX(), block.getCenterY()); 456 } 457 458 } 459 460 /** 461 * Tests this {@code XYBlockRenderer} for equality with an arbitrary 462 * object. This method returns {@code true} if and only if: 463 * <ul> 464 * <li>{@code obj} is an instance of {@code XYBlockRenderer} (not 465 * {@code null});</li> 466 * <li>{@code obj} has the same field values as this 467 * {@code XYBlockRenderer};</li> 468 * </ul> 469 * 470 * @param obj the object ({@code null} permitted). 471 * 472 * @return A boolean. 473 */ 474 @Override 475 public boolean equals(Object obj) { 476 if (obj == this) { 477 return true; 478 } 479 if (!(obj instanceof XYBlockRenderer)) { 480 return false; 481 } 482 XYBlockRenderer that = (XYBlockRenderer) obj; 483 if (this.blockHeight != that.blockHeight) { 484 return false; 485 } 486 if (this.blockWidth != that.blockWidth) { 487 return false; 488 } 489 if (!this.blockAnchor.equals(that.blockAnchor)) { 490 return false; 491 } 492 if (!this.paintScale.equals(that.paintScale)) { 493 return false; 494 } 495 if (this.drawOutlines != that.drawOutlines) { 496 return false; 497 } 498 if (this.useOutlinePaint != that.useOutlinePaint) { 499 return false; 500 } 501 return super.equals(obj); 502 } 503 504 /** 505 * Returns a clone of this renderer. 506 * 507 * @return A clone of this renderer. 508 * 509 * @throws CloneNotSupportedException if there is a problem creating the 510 * clone. 511 */ 512 @Override 513 public Object clone() throws CloneNotSupportedException { 514 XYBlockRenderer clone = (XYBlockRenderer) super.clone(); 515 if (this.paintScale instanceof PublicCloneable) { 516 PublicCloneable pc = (PublicCloneable) this.paintScale; 517 clone.paintScale = (PaintScale) pc.clone(); 518 } 519 return clone; 520 } 521 522}