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