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