001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * -------------------- 028 * XYBlockRenderer.java 029 * -------------------- 030 * (C) Copyright 2006-2008, 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 Range r = DatasetUtilities.findDomainBounds(dataset, false); 295 if (r == null) { 296 return null; 297 } 298 else { 299 return new Range(r.getLowerBound() + this.xOffset, 300 r.getUpperBound() + this.blockWidth + this.xOffset); 301 } 302 } 303 else { 304 return null; 305 } 306 } 307 308 /** 309 * Returns the range of values the renderer requires to display all the 310 * items from the specified dataset. 311 * 312 * @param dataset the dataset (<code>null</code> permitted). 313 * 314 * @return The range (<code>null</code> if the dataset is <code>null</code> 315 * or empty). 316 * 317 * @see #findDomainBounds(XYDataset) 318 */ 319 public Range findRangeBounds(XYDataset dataset) { 320 if (dataset != null) { 321 Range r = DatasetUtilities.findRangeBounds(dataset, false); 322 if (r == null) { 323 return null; 324 } 325 else { 326 return new Range(r.getLowerBound() + this.yOffset, 327 r.getUpperBound() + this.blockHeight + this.yOffset); 328 } 329 } 330 else { 331 return null; 332 } 333 } 334 335 /** 336 * Draws the block representing the specified item. 337 * 338 * @param g2 the graphics device. 339 * @param state the state. 340 * @param dataArea the data area. 341 * @param info the plot rendering info. 342 * @param plot the plot. 343 * @param domainAxis the x-axis. 344 * @param rangeAxis the y-axis. 345 * @param dataset the dataset. 346 * @param series the series index. 347 * @param item the item index. 348 * @param crosshairState the crosshair state. 349 * @param pass the pass index. 350 */ 351 public void drawItem(Graphics2D g2, XYItemRendererState state, 352 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 353 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 354 int series, int item, CrosshairState crosshairState, int pass) { 355 356 double x = dataset.getXValue(series, item); 357 double y = dataset.getYValue(series, item); 358 double z = 0.0; 359 if (dataset instanceof XYZDataset) { 360 z = ((XYZDataset) dataset).getZValue(series, item); 361 } 362 Paint p = this.paintScale.getPaint(z); 363 double xx0 = domainAxis.valueToJava2D(x + this.xOffset, dataArea, 364 plot.getDomainAxisEdge()); 365 double yy0 = rangeAxis.valueToJava2D(y + this.yOffset, dataArea, 366 plot.getRangeAxisEdge()); 367 double xx1 = domainAxis.valueToJava2D(x + this.blockWidth 368 + this.xOffset, dataArea, plot.getDomainAxisEdge()); 369 double yy1 = rangeAxis.valueToJava2D(y + this.blockHeight 370 + this.yOffset, dataArea, plot.getRangeAxisEdge()); 371 Rectangle2D block; 372 PlotOrientation orientation = plot.getOrientation(); 373 if (orientation.equals(PlotOrientation.HORIZONTAL)) { 374 block = new Rectangle2D.Double(Math.min(yy0, yy1), 375 Math.min(xx0, xx1), Math.abs(yy1 - yy0), 376 Math.abs(xx0 - xx1)); 377 } 378 else { 379 block = new Rectangle2D.Double(Math.min(xx0, xx1), 380 Math.min(yy0, yy1), Math.abs(xx1 - xx0), 381 Math.abs(yy1 - yy0)); 382 } 383 g2.setPaint(p); 384 g2.fill(block); 385 g2.setStroke(new BasicStroke(1.0f)); 386 g2.draw(block); 387 388 EntityCollection entities = state.getEntityCollection(); 389 if (entities != null) { 390 addEntity(entities, block, dataset, series, item, 0.0, 0.0); 391 } 392 393 } 394 395 /** 396 * Tests this <code>XYBlockRenderer</code> for equality with an arbitrary 397 * object. This method returns <code>true</code> if and only if: 398 * <ul> 399 * <li><code>obj</code> is an instance of <code>XYBlockRenderer</code> (not 400 * <code>null</code>);</li> 401 * <li><code>obj</code> has the same field values as this 402 * <code>XYBlockRenderer</code>;</li> 403 * </ul> 404 * 405 * @param obj the object (<code>null</code> permitted). 406 * 407 * @return A boolean. 408 */ 409 public boolean equals(Object obj) { 410 if (obj == this) { 411 return true; 412 } 413 if (!(obj instanceof XYBlockRenderer)) { 414 return false; 415 } 416 XYBlockRenderer that = (XYBlockRenderer) obj; 417 if (this.blockHeight != that.blockHeight) { 418 return false; 419 } 420 if (this.blockWidth != that.blockWidth) { 421 return false; 422 } 423 if (!this.blockAnchor.equals(that.blockAnchor)) { 424 return false; 425 } 426 if (!this.paintScale.equals(that.paintScale)) { 427 return false; 428 } 429 return super.equals(obj); 430 } 431 432 /** 433 * Returns a clone of this renderer. 434 * 435 * @return A clone of this renderer. 436 * 437 * @throws CloneNotSupportedException if there is a problem creating the 438 * clone. 439 */ 440 public Object clone() throws CloneNotSupportedException { 441 XYBlockRenderer clone = (XYBlockRenderer) super.clone(); 442 if (this.paintScale instanceof PublicCloneable) { 443 PublicCloneable pc = (PublicCloneable) this.paintScale; 444 clone.paintScale = (PaintScale) pc.clone(); 445 } 446 return clone; 447 } 448 449 }