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