001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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 * ClusteredXYBarRenderer.java
029 * ---------------------------
030 * (C) Copyright 2003-2007, 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 * 
063 */
064
065package org.jfree.chart.renderer.xy;
066
067import java.awt.GradientPaint;
068import java.awt.Graphics2D;
069import java.awt.Paint;
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.data.Range;
081import org.jfree.data.xy.IntervalXYDataset;
082import org.jfree.data.xy.XYDataset;
083import org.jfree.ui.RectangleEdge;
084import org.jfree.util.PublicCloneable;
085
086/**
087 * An extension of {@link XYBarRenderer} that displays bars for different
088 * series values at the same x next to each other. The assumption here is
089 * that for each x (time or else) there is a y value for each series. If
090 * this is not the case, there will be spaces between bars for a given x.
091 * <P>
092 * This renderer does not include code to calculate the crosshair point for the
093 * plot.
094 */
095public class ClusteredXYBarRenderer extends XYBarRenderer 
096        implements Cloneable, PublicCloneable, Serializable {
097
098    /** For serialization. */
099    private static final long serialVersionUID = 5864462149177133147L;
100    
101    /** Determines whether bar center should be interval start. */
102    private boolean centerBarAtStartValue;
103
104    /**
105     * Default constructor. Bar margin is set to 0.0.
106     */
107    public ClusteredXYBarRenderer() {
108        this(0.0, false);
109    }
110
111    /**
112     * Constructs a new XY clustered bar renderer.
113     *
114     * @param margin  the percentage amount to trim from the width of each bar.
115     * @param centerBarAtStartValue  if true, bars will be centered on the 
116     *         start of the time period.
117     */
118    public ClusteredXYBarRenderer(double margin, 
119                                  boolean centerBarAtStartValue) {
120        super(margin);
121        this.centerBarAtStartValue = centerBarAtStartValue;
122    }
123
124    /**
125     * Returns the x-value bounds for the specified dataset.
126     * 
127     * @param dataset  the dataset (<code>null</code> permitted).
128     * 
129     * @return The bounds (possibly <code>null</code>).
130     */
131    public Range findDomainBounds(XYDataset dataset) {
132        if (dataset == null) {
133            return null;
134        }
135        // need to handle cluster centering as a special case
136        if (this.centerBarAtStartValue) {
137            return findDomainBoundsWithOffset((IntervalXYDataset) dataset);
138        }
139        else {
140            return super.findDomainBounds(dataset);
141        }
142    }
143    
144    /**
145     * Iterates over the items in an {@link IntervalXYDataset} to find
146     * the range of x-values including the interval OFFSET so that it centers
147     * the interval around the start value. 
148     *  
149     * @param dataset  the dataset (<code>null</code> not permitted).
150     *   
151     * @return The range (possibly <code>null</code>).
152     */
153    protected Range findDomainBoundsWithOffset(IntervalXYDataset dataset) {
154        if (dataset == null) {
155            throw new IllegalArgumentException("Null 'dataset' argument.");   
156        }
157        double minimum = Double.POSITIVE_INFINITY;
158        double maximum = Double.NEGATIVE_INFINITY;
159        int seriesCount = dataset.getSeriesCount();
160        double lvalue;
161        double uvalue;
162        for (int series = 0; series < seriesCount; series++) {
163            int itemCount = dataset.getItemCount(series);
164            for (int item = 0; item < itemCount; item++) {
165                lvalue = dataset.getStartXValue(series, item);
166                uvalue = dataset.getEndXValue(series, item);
167                double offset = (uvalue - lvalue) / 2.0;
168                lvalue = lvalue - offset;
169                uvalue = uvalue - offset;
170                minimum = Math.min(minimum, lvalue);
171                maximum = Math.max(maximum, uvalue);
172            }
173        }
174
175        if (minimum > maximum) {
176            return null;
177        }
178        else {
179            return new Range(minimum, maximum);
180        }
181    }
182
183    /**
184     * Draws the visual representation of a single data item. This method
185     * is mostly copied from the superclass, the change is that in the
186     * calculated space for a singe bar we draw bars for each series next to
187     * each other. The width of each bar is the available width divided by
188     * the number of series. Bars for each series are drawn in order left to
189     * right.
190     *
191     * @param g2  the graphics device.
192     * @param state  the renderer state.
193     * @param dataArea  the area within which the plot is being drawn.
194     * @param info  collects information about the drawing.
195     * @param plot  the plot (can be used to obtain standard color 
196     *              information etc).
197     * @param domainAxis  the domain axis.
198     * @param rangeAxis  the range axis.
199     * @param dataset  the dataset.
200     * @param series  the series index.
201     * @param item  the item index.
202     * @param crosshairState  crosshair information for the plot 
203     *                        (<code>null</code> permitted).
204     * @param pass  the pass index.
205     */
206    public void drawItem(Graphics2D g2,
207                         XYItemRendererState state,
208                         Rectangle2D dataArea,
209                         PlotRenderingInfo info,
210                         XYPlot plot, 
211                         ValueAxis domainAxis, 
212                         ValueAxis rangeAxis,
213                         XYDataset dataset, int series, int item,
214                         CrosshairState crosshairState,
215                         int pass) {
216
217        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
218
219        double y0;
220        double y1;
221        if (getUseYInterval()) {
222            y0 = intervalDataset.getStartYValue(series, item);
223            y1 = intervalDataset.getEndYValue(series, item);
224        }
225        else {
226            y0 = getBase();
227            y1 = intervalDataset.getYValue(series, item);
228        }
229        if (Double.isNaN(y0) || Double.isNaN(y1)) {
230            return;
231        }
232
233        double yy0 = rangeAxis.valueToJava2D(y0, dataArea, 
234                plot.getRangeAxisEdge());
235        double yy1 = rangeAxis.valueToJava2D(y1, dataArea, 
236                plot.getRangeAxisEdge());
237
238        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
239        double x0 = intervalDataset.getStartXValue(series, item);
240        double xx0 = domainAxis.valueToJava2D(x0, dataArea, xAxisLocation);
241        
242        double x1 = intervalDataset.getEndXValue(series, item);
243        double xx1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
244        
245        double intervalW = xx1 - xx0;  // this may be negative
246        double baseX = xx0;
247        if (this.centerBarAtStartValue) {
248            baseX = baseX - intervalW / 2.0;
249        }
250        double m = getMargin();
251        if (m > 0.0) {
252            double cut = intervalW * getMargin();
253            intervalW = intervalW - cut;
254            baseX = baseX + (cut / 2);
255        }
256        
257        double intervalH = Math.abs(yy0 - yy1);  // we don't need the sign
258
259        PlotOrientation orientation = plot.getOrientation();        
260
261        int numSeries = dataset.getSeriesCount();
262        double seriesBarWidth = intervalW / numSeries;  // may be negative
263
264        Rectangle2D bar = null;
265        if (orientation == PlotOrientation.HORIZONTAL) {
266            double barY0 = baseX + (seriesBarWidth * series);
267            double barY1 = barY0 + seriesBarWidth;
268            double rx = Math.min(yy0, yy1);
269            double rw = intervalH;
270            double ry = Math.min(barY0, barY1);
271            double rh = Math.abs(barY1 - barY0);
272            bar = new Rectangle2D.Double(rx, ry, rw, rh);
273        }
274        else if (orientation == PlotOrientation.VERTICAL) {
275            double barX0 = baseX + (seriesBarWidth * series);
276            double barX1 = barX0 + seriesBarWidth;
277            double rx = Math.min(barX0, barX1);
278            double rw = Math.abs(barX1 - barX0);
279            double ry = Math.min(yy0, yy1);;
280            double rh = intervalH;
281            bar = new Rectangle2D.Double(rx, ry, rw, rh);
282        }
283        Paint itemPaint = getItemPaint(series, item);
284        if (getGradientPaintTransformer() 
285                != null && itemPaint instanceof GradientPaint) {
286            GradientPaint gp = (GradientPaint) itemPaint;
287            itemPaint = getGradientPaintTransformer().transform(gp, bar);
288        }
289        g2.setPaint(itemPaint);
290
291        g2.fill(bar);
292        if (isDrawBarOutline() && Math.abs(seriesBarWidth) > 3) {
293            g2.setStroke(getItemOutlineStroke(series, item));
294            g2.setPaint(getItemOutlinePaint(series, item));
295            g2.draw(bar);
296        }
297
298        if (isItemLabelVisible(series, item)) {
299            XYItemLabelGenerator generator = getItemLabelGenerator(series, 
300                    item);
301            drawItemLabel(g2, dataset, series, item, plot, generator, bar, 
302                    y1 < 0.0);
303        }
304
305        // add an entity for the item...
306        if (info != null) {
307            EntityCollection entities = info.getOwner().getEntityCollection();
308            if (entities != null) {
309                addEntity(entities, bar, dataset, series, item, 
310                        bar.getCenterX(), bar.getCenterY());
311            }
312        }
313
314    }
315
316    /**
317     * Tests this renderer for equality with an arbitrary object, returning
318     * <code>true</code> if <code>obj</code> is a 
319     * <code>ClusteredXYBarRenderer</code> with the same settings as this
320     * renderer, and <code>false</code> otherwise.
321     * 
322     * @param obj  the object (<code>null</code> permitted).
323     * 
324     * @return A boolean.
325     */
326    public boolean equals(Object obj) {
327        if (obj == this) {
328            return true;
329        }
330        if (!(obj instanceof ClusteredXYBarRenderer)) {
331            return false;
332        }
333        ClusteredXYBarRenderer that = (ClusteredXYBarRenderer) obj;
334        if (this.centerBarAtStartValue != that.centerBarAtStartValue) {
335            return false;
336        }
337        return super.equals(obj);
338    }
339    
340    /**
341     * Returns a clone of the renderer.
342     * 
343     * @return A clone.
344     * 
345     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
346     */
347    public Object clone() throws CloneNotSupportedException {
348        return super.clone();
349    }
350    
351}