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 &gt; 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 &gt; 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}