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    }