001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, 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-2022, 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.Graphics2D;
040import java.awt.Paint;
041import java.awt.geom.Rectangle2D;
042import java.io.Serializable;
043
044import org.jfree.chart.axis.ValueAxis;
045import org.jfree.chart.entity.EntityCollection;
046import org.jfree.chart.event.RendererChangeEvent;
047import org.jfree.chart.plot.CrosshairState;
048import org.jfree.chart.plot.PlotOrientation;
049import org.jfree.chart.plot.PlotRenderingInfo;
050import org.jfree.chart.plot.XYPlot;
051import org.jfree.chart.renderer.LookupPaintScale;
052import org.jfree.chart.renderer.PaintScale;
053import org.jfree.chart.api.RectangleAnchor;
054import org.jfree.chart.internal.Args;
055import org.jfree.chart.api.PublicCloneable;
056import org.jfree.data.Range;
057import org.jfree.data.general.DatasetUtils;
058import org.jfree.data.xy.XYDataset;
059import org.jfree.data.xy.XYZDataset;
060
061/**
062 * A renderer that represents data from an {@link XYZDataset} by drawing a
063 * color block at each (x, y) point, where the color is a function of the
064 * z-value from the dataset.  The example shown here is generated by the
065 * {@code XYBlockChartDemo1.java} program included in the JFreeChart
066 * demo collection:
067 * <br><br>
068 * <img src="doc-files/XYBlockRendererSample.png" alt="XYBlockRendererSample.png">
069 */
070public class XYBlockRenderer extends AbstractXYItemRenderer
071        implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
072
073    /**
074     * The block width (defaults to 1.0).
075     */
076    private double blockWidth = 1.0;
077
078    /**
079     * The block height (defaults to 1.0).
080     */
081    private double blockHeight = 1.0;
082
083    /**
084     * The anchor point used to align each block to its (x, y) location.  The
085     * default value is {@code RectangleAnchor.CENTER}.
086     */
087    private RectangleAnchor blockAnchor = RectangleAnchor.CENTER;
088
089    /** Temporary storage for the x-offset used to align the block anchor. */
090    private double xOffset;
091
092    /** Temporary storage for the y-offset used to align the block anchor. */
093    private double yOffset;
094
095    /** The paint scale. */
096    private PaintScale paintScale;
097    
098    /** A flag that controls whether outlines are drawn for blocks. */
099    private boolean drawOutlines;
100
101    /**
102     * A flag that controls whether the outline paint is used for drawing block
103     * outlines.
104     */
105    private boolean useOutlinePaint;
106
107    /**
108     * Creates a new {@code XYBlockRenderer} instance with default
109     * attributes.
110     */
111    public XYBlockRenderer() {
112        updateOffsets();
113        this.paintScale = new LookupPaintScale();
114        this.drawOutlines = true;
115        this.useOutlinePaint = false; // use item paint by default
116    }
117
118    /**
119     * Returns the block width, in data/axis units.
120     *
121     * @return The block width.
122     *
123     * @see #setBlockWidth(double)
124     */
125    public double getBlockWidth() {
126        return this.blockWidth;
127    }
128
129    /**
130     * Sets the width of the blocks used to represent each data item and
131     * sends a {@link RendererChangeEvent} to all registered listeners.
132     *
133     * @param width  the new width, in data/axis units (must be &gt; 0.0).
134     *
135     * @see #getBlockWidth()
136     */
137    public void setBlockWidth(double width) {
138        if (width <= 0.0) {
139            throw new IllegalArgumentException(
140                    "The 'width' argument must be > 0.0");
141        }
142        this.blockWidth = width;
143        updateOffsets();
144        fireChangeEvent();
145    }
146
147    /**
148     * Returns the block height, in data/axis units.
149     *
150     * @return The block height.
151     *
152     * @see #setBlockHeight(double)
153     */
154    public double getBlockHeight() {
155        return this.blockHeight;
156    }
157
158    /**
159     * Sets the height of the blocks used to represent each data item and
160     * sends a {@link RendererChangeEvent} to all registered listeners.
161     *
162     * @param height  the new height, in data/axis units (must be &gt; 0.0).
163     *
164     * @see #getBlockHeight()
165     */
166    public void setBlockHeight(double height) {
167        if (height <= 0.0) {
168            throw new IllegalArgumentException(
169                    "The 'height' argument must be > 0.0");
170        }
171        this.blockHeight = height;
172        updateOffsets();
173        fireChangeEvent();
174    }
175
176    /**
177     * Returns the anchor point used to align a block at its (x, y) location.
178     * The default values is {@link RectangleAnchor#CENTER}.
179     *
180     * @return The anchor point (never {@code null}).
181     *
182     * @see #setBlockAnchor(RectangleAnchor)
183     */
184    public RectangleAnchor getBlockAnchor() {
185        return this.blockAnchor;
186    }
187
188    /**
189     * Sets the anchor point used to align a block at its (x, y) location and
190     * sends a {@link RendererChangeEvent} to all registered listeners.
191     *
192     * @param anchor  the anchor.
193     *
194     * @see #getBlockAnchor()
195     */
196    public void setBlockAnchor(RectangleAnchor anchor) {
197        Args.nullNotPermitted(anchor, "anchor");
198        if (this.blockAnchor.equals(anchor)) {
199            return;  // no change
200        }
201        this.blockAnchor = anchor;
202        updateOffsets();
203        fireChangeEvent();
204    }
205
206    /**
207     * Returns the paint scale used by the renderer.
208     *
209     * @return The paint scale (never {@code null}).
210     *
211     * @see #setPaintScale(PaintScale)
212     */
213    public PaintScale getPaintScale() {
214        return this.paintScale;
215    }
216
217    /**
218     * Sets the paint scale used by the renderer and sends a
219     * {@link RendererChangeEvent} to all registered listeners.
220     *
221     * @param scale  the scale ({@code null} not permitted).
222     *
223     * @see #getPaintScale()
224     */
225    public void setPaintScale(PaintScale scale) {
226        Args.nullNotPermitted(scale, "scale");
227        this.paintScale = scale;
228        fireChangeEvent();
229    }
230
231    /**
232     * Returns {@code true} if outlines should be drawn for blocks, and
233     * {@code false} otherwise.  The default value is {@code true}.
234     *
235     * @return A boolean.
236     *
237     * @see #setDrawOutlines(boolean)
238     */
239    public boolean getDrawOutlines() {
240        return this.drawOutlines;
241    }
242
243    /**
244     * Sets the flag that controls whether outlines are drawn for
245     * blocks, and sends a {@link RendererChangeEvent} to all registered
246     * listeners.
247     *
248     * @param flag  the flag.
249     *
250     * @see #getDrawOutlines()
251     */
252    public void setDrawOutlines(boolean flag) {
253        this.drawOutlines = flag;
254        fireChangeEvent();
255    }
256
257    /**
258     * Returns {@code true} if the renderer should use the outline paint
259     * setting to draw block outlines, and {@code false} if it should just
260     * use the regular item paint.
261     *
262     * @return A boolean.
263     *
264     * @see #setUseOutlinePaint(boolean)
265     */
266    public boolean getUseOutlinePaint() {
267        return this.useOutlinePaint;
268    }
269
270    /**
271     * Sets the flag that controls whether the outline paint is used to draw
272     * block outlines, and sends a {@link RendererChangeEvent} to all
273     * registered listeners.
274     *
275     * @param flag  the flag.
276     *
277     * @see #getUseOutlinePaint()
278     */
279    public void setUseOutlinePaint(boolean flag) {
280        this.useOutlinePaint = flag;
281        fireChangeEvent();
282    }
283
284    /**
285     * Updates the offsets to take into account the block width, height and
286     * anchor.
287     */
288    private void updateOffsets() {
289        if (this.blockAnchor.equals(RectangleAnchor.BOTTOM_LEFT)) {
290            this.xOffset = 0.0;
291            this.yOffset = 0.0;
292        }
293        else if (this.blockAnchor.equals(RectangleAnchor.BOTTOM)) {
294            this.xOffset = -this.blockWidth / 2.0;
295            this.yOffset = 0.0;
296        }
297        else if (this.blockAnchor.equals(RectangleAnchor.BOTTOM_RIGHT)) {
298            this.xOffset = -this.blockWidth;
299            this.yOffset = 0.0;
300        }
301        else if (this.blockAnchor.equals(RectangleAnchor.LEFT)) {
302            this.xOffset = 0.0;
303            this.yOffset = -this.blockHeight / 2.0;
304        }
305        else if (this.blockAnchor.equals(RectangleAnchor.CENTER)) {
306            this.xOffset = -this.blockWidth / 2.0;
307            this.yOffset = -this.blockHeight / 2.0;
308        }
309        else if (this.blockAnchor.equals(RectangleAnchor.RIGHT)) {
310            this.xOffset = -this.blockWidth;
311            this.yOffset = -this.blockHeight / 2.0;
312        }
313        else if (this.blockAnchor.equals(RectangleAnchor.TOP_LEFT)) {
314            this.xOffset = 0.0;
315            this.yOffset = -this.blockHeight;
316        }
317        else if (this.blockAnchor.equals(RectangleAnchor.TOP)) {
318            this.xOffset = -this.blockWidth / 2.0;
319            this.yOffset = -this.blockHeight;
320        }
321        else if (this.blockAnchor.equals(RectangleAnchor.TOP_RIGHT)) {
322            this.xOffset = -this.blockWidth;
323            this.yOffset = -this.blockHeight;
324        }
325    }
326
327    /**
328     * Returns the lower and upper bounds (range) of the x-values in the
329     * specified dataset.
330     *
331     * @param dataset  the dataset ({@code null} permitted).
332     *
333     * @return The range ({@code null} if the dataset is {@code null}
334     *         or empty).
335     *
336     * @see #findRangeBounds(XYDataset)
337     */
338    @Override
339    public Range findDomainBounds(XYDataset dataset) {
340        if (dataset == null) {
341            return null;
342        }
343        Range r = DatasetUtils.findDomainBounds(dataset, false);
344        if (r == null) {
345            return null;
346        }
347        return new Range(r.getLowerBound() + this.xOffset,
348                         r.getUpperBound() + this.blockWidth + this.xOffset);
349    }
350
351    /**
352     * Returns the range of values the renderer requires to display all the
353     * items from the specified dataset.
354     *
355     * @param dataset  the dataset ({@code null} permitted).
356     *
357     * @return The range ({@code null} if the dataset is {@code null}
358     *         or empty).
359     *
360     * @see #findDomainBounds(XYDataset)
361     */
362    @Override
363    public Range findRangeBounds(XYDataset dataset) {
364        if (dataset != null) {
365            Range r = DatasetUtils.findRangeBounds(dataset, false);
366            if (r == null) {
367                return null;
368            }
369            else {
370                return new Range(r.getLowerBound() + this.yOffset,
371                        r.getUpperBound() + this.blockHeight + this.yOffset);
372            }
373        }
374        else {
375            return null;
376        }
377    }
378
379    /**
380     * Draws the block representing the specified item.
381     *
382     * @param g2  the graphics device.
383     * @param state  the state.
384     * @param dataArea  the data area.
385     * @param info  the plot rendering info.
386     * @param plot  the plot.
387     * @param domainAxis  the x-axis.
388     * @param rangeAxis  the y-axis.
389     * @param dataset  the dataset.
390     * @param series  the series index.
391     * @param item  the item index.
392     * @param crosshairState  the crosshair state.
393     * @param pass  the pass index.
394     */
395    @Override
396    public void drawItem(Graphics2D g2, XYItemRendererState state,
397            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
398            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
399            int series, int item, CrosshairState crosshairState, int pass) {
400
401        double x = dataset.getXValue(series, item);
402        double y = dataset.getYValue(series, item);
403        double z = 0.0;
404        if (dataset instanceof XYZDataset) {
405            z = ((XYZDataset) dataset).getZValue(series, item);
406        }
407
408        Paint p = this.paintScale.getPaint(z);
409        double xx0 = domainAxis.valueToJava2D(x + this.xOffset, dataArea,
410                plot.getDomainAxisEdge());
411        double yy0 = rangeAxis.valueToJava2D(y + this.yOffset, dataArea,
412                plot.getRangeAxisEdge());
413        double xx1 = domainAxis.valueToJava2D(x + this.blockWidth
414                + this.xOffset, dataArea, plot.getDomainAxisEdge());
415        double yy1 = rangeAxis.valueToJava2D(y + this.blockHeight
416                + this.yOffset, dataArea, plot.getRangeAxisEdge());
417        Rectangle2D block;
418        PlotOrientation orientation = plot.getOrientation();
419        if (orientation.equals(PlotOrientation.HORIZONTAL)) {
420            block = new Rectangle2D.Double(Math.min(yy0, yy1),
421                    Math.min(xx0, xx1), Math.abs(yy1 - yy0),
422                    Math.abs(xx0 - xx1));
423        }
424        else {
425            block = new Rectangle2D.Double(Math.min(xx0, xx1),
426                    Math.min(yy0, yy1), Math.abs(xx1 - xx0),
427                    Math.abs(yy1 - yy0));
428        }
429        g2.setPaint(p);
430        g2.fill(block);
431        if (getDrawOutlines()) {
432            if (getUseOutlinePaint()) {
433                g2.setPaint(getItemOutlinePaint(series, item));
434            }
435            g2.setStroke(lookupSeriesOutlineStroke(series));
436            g2.draw(block);
437        }
438
439        if (isItemLabelVisible(series, item)) {
440            drawItemLabel(g2, orientation, dataset, series, item, 
441                    block.getCenterX(), block.getCenterY(), y < 0.0);
442        }
443
444        int datasetIndex = plot.indexOf(dataset);
445        double transX = domainAxis.valueToJava2D(x, dataArea,
446                plot.getDomainAxisEdge());
447        double transY = rangeAxis.valueToJava2D(y, dataArea,
448                plot.getRangeAxisEdge());        
449        updateCrosshairValues(crosshairState, x, y, datasetIndex,
450                transX, transY, orientation);
451
452        EntityCollection entities = state.getEntityCollection();
453        if (entities != null) {
454            addEntity(entities, block, dataset, series, item, 
455                    block.getCenterX(), block.getCenterY());
456        }
457
458    }
459
460    /**
461     * Tests this {@code XYBlockRenderer} for equality with an arbitrary
462     * object.  This method returns {@code true} if and only if:
463     * <ul>
464     * <li>{@code obj} is an instance of {@code XYBlockRenderer} (not
465     *     {@code null});</li>
466     * <li>{@code obj} has the same field values as this
467     *     {@code XYBlockRenderer};</li>
468     * </ul>
469     *
470     * @param obj  the object ({@code null} permitted).
471     *
472     * @return A boolean.
473     */
474    @Override
475    public boolean equals(Object obj) {
476        if (obj == this) {
477            return true;
478        }
479        if (!(obj instanceof XYBlockRenderer)) {
480            return false;
481        }
482        XYBlockRenderer that = (XYBlockRenderer) obj;
483        if (this.blockHeight != that.blockHeight) {
484            return false;
485        }
486        if (this.blockWidth != that.blockWidth) {
487            return false;
488        }
489        if (!this.blockAnchor.equals(that.blockAnchor)) {
490            return false;
491        }
492        if (!this.paintScale.equals(that.paintScale)) {
493            return false;
494        }
495        if (this.drawOutlines != that.drawOutlines) {
496            return false;
497        }
498        if (this.useOutlinePaint != that.useOutlinePaint) {
499            return false;
500        }
501        return super.equals(obj);
502    }
503
504    /**
505     * Returns a clone of this renderer.
506     *
507     * @return A clone of this renderer.
508     *
509     * @throws CloneNotSupportedException if there is a problem creating the
510     *     clone.
511     */
512    @Override
513    public Object clone() throws CloneNotSupportedException {
514        XYBlockRenderer clone = (XYBlockRenderer) super.clone();
515        if (this.paintScale instanceof PublicCloneable) {
516            PublicCloneable pc = (PublicCloneable) this.paintScale;
517            clone.paintScale = (PaintScale) pc.clone();
518        }
519        return clone;
520    }
521
522}