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 }