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