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