001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * ClusteredXYBarRenderer.java
029 * ---------------------------
030 * (C) Copyright 2003-2014, by Paolo Cova and Contributors.
031 *
032 * Original Author:  Paolo Cova;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *                   Christian W. Zuckschwerdt;
035 *                   Matthias Rose;
036 *
037 * Changes
038 * -------
039 * 24-Jan-2003 : Version 1, contributed by Paolo Cova (DG);
040 * 25-Mar-2003 : Implemented Serializable (DG);
041 * 01-May-2003 : Modified drawItem() method signature (DG);
042 * 30-Jul-2003 : Modified entity constructor (CZ);
043 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
044 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
045 * 07-Oct-2003 : Added renderer state (DG);
046 * 03-Nov-2003 : In draw method added state parameter and y==null value
047 *               handling (MR);
048 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
049 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
050 *               getYValue() (DG);
051 * 01-Oct-2004 : Fixed bug where 'drawBarOutline' flag is ignored (DG);
052 * 16-May-2005 : Fixed to used outline stroke for bar outlines.  Removed some
053 *               redundant code with the result that the renderer now respects
054 *               the 'base' setting from the super-class. Added an equals()
055 *               method (DG);
056 * 19-May-2005 : Added minimal item label implementation - needs improving (DG);
057 * ------------- JFREECHART 1.0.x ---------------------------------------------
058 * 11-Dec-2006 : Added support for GradientPaint (DG);
059 * 12-Jun-2007 : Added override to findDomainBounds() to handle cluster offset,
060 *               fixed rendering to handle inverted axes, and simplified
061 *               entity generation code (DG);
062 * 24-Jun-2008 : Added new barPainter mechanism (DG);
063 * 03-Jul-2013 : Use ParamChecks (DG);
064 *
065 */
066
067package org.jfree.chart.renderer.xy;
068
069import java.awt.Graphics2D;
070import java.awt.geom.Rectangle2D;
071import java.io.Serializable;
072
073import org.jfree.chart.axis.ValueAxis;
074import org.jfree.chart.entity.EntityCollection;
075import org.jfree.chart.labels.XYItemLabelGenerator;
076import org.jfree.chart.plot.CrosshairState;
077import org.jfree.chart.plot.PlotOrientation;
078import org.jfree.chart.plot.PlotRenderingInfo;
079import org.jfree.chart.plot.XYPlot;
080import org.jfree.chart.util.ParamChecks;
081import org.jfree.data.Range;
082import org.jfree.data.xy.IntervalXYDataset;
083import org.jfree.data.xy.XYDataset;
084import org.jfree.ui.RectangleEdge;
085import org.jfree.util.PublicCloneable;
086
087/**
088 * An extension of {@link XYBarRenderer} that displays bars for different
089 * series values at the same x next to each other. The assumption here is
090 * that for each x (time or else) there is a y value for each series. If
091 * this is not the case, there will be spaces between bars for a given x.
092 * The example shown here is generated by the
093 * <code>ClusteredXYBarRendererDemo1.java</code> program included in the
094 * JFreeChart demo collection:
095 * <br><br>
096 * <img src="../../../../../images/ClusteredXYBarRendererSample.png"
097 * alt="ClusteredXYBarRendererSample.png">
098 * <P>
099 * This renderer does not include code to calculate the crosshair point for the
100 * plot.
101 */
102public class ClusteredXYBarRenderer extends XYBarRenderer
103        implements Cloneable, PublicCloneable, Serializable {
104
105    /** For serialization. */
106    private static final long serialVersionUID = 5864462149177133147L;
107
108    /** Determines whether bar center should be interval start. */
109    private boolean centerBarAtStartValue;
110
111    /**
112     * Default constructor. Bar margin is set to 0.0.
113     */
114    public ClusteredXYBarRenderer() {
115        this(0.0, false);
116    }
117
118    /**
119     * Constructs a new XY clustered bar renderer.
120     *
121     * @param margin  the percentage amount to trim from the width of each bar.
122     * @param centerBarAtStartValue  if true, bars will be centered on the
123     *         start of the time period.
124     */
125    public ClusteredXYBarRenderer(double margin,
126                                  boolean centerBarAtStartValue) {
127        super(margin);
128        this.centerBarAtStartValue = centerBarAtStartValue;
129    }
130
131    /**
132     * Returns the number of passes through the dataset that this renderer
133     * requires.  In this case, two passes are required, the first for drawing
134     * the shadows (if visible), and the second for drawing the bars.
135     *
136     * @return <code>2</code>.
137     */
138    @Override
139    public int getPassCount() {
140        return 2;
141    }
142
143    /**
144     * Returns the x-value bounds for the specified dataset.
145     *
146     * @param dataset  the dataset (<code>null</code> permitted).
147     *
148     * @return The bounds (possibly <code>null</code>).
149     */
150    @Override
151    public Range findDomainBounds(XYDataset dataset) {
152        if (dataset == null) {
153            return null;
154        }
155        // need to handle cluster centering as a special case
156        if (this.centerBarAtStartValue) {
157            return findDomainBoundsWithOffset((IntervalXYDataset) dataset);
158        }
159        else {
160            return super.findDomainBounds(dataset);
161        }
162    }
163
164    /**
165     * Iterates over the items in an {@link IntervalXYDataset} to find
166     * the range of x-values including the interval OFFSET so that it centers
167     * the interval around the start value.
168     *
169     * @param dataset  the dataset (<code>null</code> not permitted).
170     *
171     * @return The range (possibly <code>null</code>).
172     */
173    protected Range findDomainBoundsWithOffset(IntervalXYDataset dataset) {
174        ParamChecks.nullNotPermitted(dataset, "dataset");
175        double minimum = Double.POSITIVE_INFINITY;
176        double maximum = Double.NEGATIVE_INFINITY;
177        int seriesCount = dataset.getSeriesCount();
178        double lvalue;
179        double uvalue;
180        for (int series = 0; series < seriesCount; series++) {
181            int itemCount = dataset.getItemCount(series);
182            for (int item = 0; item < itemCount; item++) {
183                lvalue = dataset.getStartXValue(series, item);
184                uvalue = dataset.getEndXValue(series, item);
185                double offset = (uvalue - lvalue) / 2.0;
186                lvalue = lvalue - offset;
187                uvalue = uvalue - offset;
188                minimum = Math.min(minimum, lvalue);
189                maximum = Math.max(maximum, uvalue);
190            }
191        }
192
193        if (minimum > maximum) {
194            return null;
195        }
196        else {
197            return new Range(minimum, maximum);
198        }
199    }
200
201    /**
202     * Draws the visual representation of a single data item. This method
203     * is mostly copied from the superclass, the change is that in the
204     * calculated space for a singe bar we draw bars for each series next to
205     * each other. The width of each bar is the available width divided by
206     * the number of series. Bars for each series are drawn in order left to
207     * right.
208     *
209     * @param g2  the graphics device.
210     * @param state  the renderer state.
211     * @param dataArea  the area within which the plot is being drawn.
212     * @param info  collects information about the drawing.
213     * @param plot  the plot (can be used to obtain standard color
214     *              information etc).
215     * @param domainAxis  the domain axis.
216     * @param rangeAxis  the range axis.
217     * @param dataset  the dataset.
218     * @param series  the series index.
219     * @param item  the item index.
220     * @param crosshairState  crosshair information for the plot
221     *                        (<code>null</code> permitted).
222     * @param pass  the pass index.
223     */
224    @Override
225    public void drawItem(Graphics2D g2, XYItemRendererState state, 
226            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
227            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 
228            int series, int item, CrosshairState crosshairState, int pass) {
229
230        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
231
232        double y0;
233        double y1;
234        if (getUseYInterval()) {
235            y0 = intervalDataset.getStartYValue(series, item);
236            y1 = intervalDataset.getEndYValue(series, item);
237        }
238        else {
239            y0 = getBase();
240            y1 = intervalDataset.getYValue(series, item);
241        }
242        if (Double.isNaN(y0) || Double.isNaN(y1)) {
243            return;
244        }
245
246        double yy0 = rangeAxis.valueToJava2D(y0, dataArea,
247                plot.getRangeAxisEdge());
248        double yy1 = rangeAxis.valueToJava2D(y1, dataArea,
249                plot.getRangeAxisEdge());
250
251        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
252        double x0 = intervalDataset.getStartXValue(series, item);
253        double xx0 = domainAxis.valueToJava2D(x0, dataArea, xAxisLocation);
254
255        double x1 = intervalDataset.getEndXValue(series, item);
256        double xx1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
257
258        double intervalW = xx1 - xx0;  // this may be negative
259        double baseX = xx0;
260        if (this.centerBarAtStartValue) {
261            baseX = baseX - intervalW / 2.0;
262        }
263        double m = getMargin();
264        if (m > 0.0) {
265            double cut = intervalW * getMargin();
266            intervalW = intervalW - cut;
267            baseX = baseX + (cut / 2);
268        }
269
270        double intervalH = Math.abs(yy0 - yy1);  // we don't need the sign
271
272        PlotOrientation orientation = plot.getOrientation();
273
274        int numSeries = dataset.getSeriesCount();
275        double seriesBarWidth = intervalW / numSeries;  // may be negative
276
277        Rectangle2D bar = null;
278        if (orientation == PlotOrientation.HORIZONTAL) {
279            double barY0 = baseX + (seriesBarWidth * series);
280            double barY1 = barY0 + seriesBarWidth;
281            double rx = Math.min(yy0, yy1);
282            double rw = intervalH;
283            double ry = Math.min(barY0, barY1);
284            double rh = Math.abs(barY1 - barY0);
285            bar = new Rectangle2D.Double(rx, ry, rw, rh);
286        }
287        else if (orientation == PlotOrientation.VERTICAL) {
288            double barX0 = baseX + (seriesBarWidth * series);
289            double barX1 = barX0 + seriesBarWidth;
290            double rx = Math.min(barX0, barX1);
291            double rw = Math.abs(barX1 - barX0);
292            double ry = Math.min(yy0, yy1);
293            double rh = intervalH;
294            bar = new Rectangle2D.Double(rx, ry, rw, rh);
295        } else {
296            throw new IllegalStateException();
297        }
298        boolean positive = (y1 > 0.0);
299        boolean inverted = rangeAxis.isInverted();
300        RectangleEdge barBase;
301        if (orientation == PlotOrientation.HORIZONTAL) {
302            if (positive && inverted || !positive && !inverted) {
303                barBase = RectangleEdge.RIGHT;
304            }
305            else {
306                barBase = RectangleEdge.LEFT;
307            }
308        }
309        else {
310            if (positive && !inverted || !positive && inverted) {
311                barBase = RectangleEdge.BOTTOM;
312            }
313            else {
314                barBase = RectangleEdge.TOP;
315            }
316        }
317        if (pass == 0 && getShadowsVisible()) {
318            getBarPainter().paintBarShadow(g2, this, series, item, bar, barBase,
319                !getUseYInterval());
320        }
321        if (pass == 1) {
322            getBarPainter().paintBar(g2, this, series, item, bar, barBase);
323
324            if (isItemLabelVisible(series, item)) {
325                XYItemLabelGenerator generator = getItemLabelGenerator(series,
326                        item);
327                drawItemLabel(g2, dataset, series, item, plot, generator, bar,
328                        y1 < 0.0);
329            }
330
331            // add an entity for the item...
332            if (info != null) {
333                EntityCollection entities
334                        = info.getOwner().getEntityCollection();
335                if (entities != null) {
336                    addEntity(entities, bar, dataset, series, item,
337                            bar.getCenterX(), bar.getCenterY());
338                }
339            }
340        }
341
342    }
343
344    /**
345     * Tests this renderer for equality with an arbitrary object, returning
346     * <code>true</code> if <code>obj</code> is a
347     * <code>ClusteredXYBarRenderer</code> with the same settings as this
348     * renderer, and <code>false</code> otherwise.
349     *
350     * @param obj  the object (<code>null</code> permitted).
351     *
352     * @return A boolean.
353     */
354    @Override
355    public boolean equals(Object obj) {
356        if (obj == this) {
357            return true;
358        }
359        if (!(obj instanceof ClusteredXYBarRenderer)) {
360            return false;
361        }
362        ClusteredXYBarRenderer that = (ClusteredXYBarRenderer) obj;
363        if (this.centerBarAtStartValue != that.centerBarAtStartValue) {
364            return false;
365        }
366        return super.equals(obj);
367    }
368
369    /**
370     * Returns a clone of the renderer.
371     *
372     * @return A clone.
373     *
374     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
375     */
376    @Override
377    public Object clone() throws CloneNotSupportedException {
378        return super.clone();
379    }
380
381}