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