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 * StackedXYBarRenderer.java
029 * -------------------------
030 * (C) Copyright 2004-2007, by Andreas Schroeder and Contributors.
031 *
032 * Original Author:  Andreas Schroeder;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *
035 * Changes
036 * -------
037 * 01-Apr-2004 : Version 1 (AS);
038 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
039 *               getYValue() (DG);
040 * 15-Aug-2004 : Added drawBarOutline to control draw/don't-draw bar 
041 *               outlines (BN);
042 * 10-Sep-2004 : drawBarOutline attribute is now inherited from XYBarRenderer 
043 *               and double primitives are retrieved from the dataset rather 
044 *               than Number objects (DG);
045 * 07-Jan-2005 : Updated for method name change in DatasetUtilities (DG);
046 * 25-Jan-2005 : Modified to handle negative values correctly (DG);
047 * ------------- JFREECHART 1.0.x ---------------------------------------------
048 * 06-Dec-2006 : Added support for GradientPaint (DG);
049 * 15-Mar-2007 : Added renderAsPercentages option (DG);
050 * 
051 */
052
053package org.jfree.chart.renderer.xy;
054
055import java.awt.GradientPaint;
056import java.awt.Graphics2D;
057import java.awt.Paint;
058import java.awt.geom.Rectangle2D;
059
060import org.jfree.chart.axis.ValueAxis;
061import org.jfree.chart.entity.EntityCollection;
062import org.jfree.chart.event.RendererChangeEvent;
063import org.jfree.chart.labels.ItemLabelAnchor;
064import org.jfree.chart.labels.ItemLabelPosition;
065import org.jfree.chart.labels.XYItemLabelGenerator;
066import org.jfree.chart.plot.CrosshairState;
067import org.jfree.chart.plot.PlotOrientation;
068import org.jfree.chart.plot.PlotRenderingInfo;
069import org.jfree.chart.plot.XYPlot;
070import org.jfree.data.Range;
071import org.jfree.data.general.DatasetUtilities;
072import org.jfree.data.xy.IntervalXYDataset;
073import org.jfree.data.xy.TableXYDataset;
074import org.jfree.data.xy.XYDataset;
075import org.jfree.ui.RectangleEdge;
076import org.jfree.ui.TextAnchor;
077
078/**
079 * A bar renderer that displays the series items stacked.
080 * The dataset used together with this renderer must be a
081 * {@link org.jfree.data.xy.IntervalXYDataset} and a
082 * {@link org.jfree.data.xy.TableXYDataset}. For example, the
083 * dataset class {@link org.jfree.data.xy.CategoryTableXYDataset}
084 * implements both interfaces.
085 */
086public class StackedXYBarRenderer extends XYBarRenderer {
087  
088    /** For serialization. */
089    private static final long serialVersionUID = -7049101055533436444L;
090    
091    /** A flag that controls whether the bars display values or percentages. */
092    private boolean renderAsPercentages;
093
094    /**
095     * Creates a new renderer.
096     */
097    public StackedXYBarRenderer() {
098        this(0.0);
099    }
100
101    /**
102     * Creates a new renderer.
103     *
104     * @param margin  the percentual amount of the bars that are cut away.
105     */
106    public StackedXYBarRenderer(double margin) {
107        super(margin);
108        this.renderAsPercentages = false;
109        
110        // set the default item label positions, which will only be used if 
111        // the user requests visible item labels...
112        ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 
113                TextAnchor.CENTER);
114        setBasePositiveItemLabelPosition(p);
115        setBaseNegativeItemLabelPosition(p);
116        setPositiveItemLabelPositionFallback(null);
117        setNegativeItemLabelPositionFallback(null);
118    }
119
120    /**
121     * Returns <code>true</code> if the renderer displays each item value as
122     * a percentage (so that the stacked bars add to 100%), and 
123     * <code>false</code> otherwise.
124     * 
125     * @return A boolean.
126     * 
127     * @see #setRenderAsPercentages(boolean)
128     * 
129     * @since 1.0.5
130     */
131    public boolean getRenderAsPercentages() {
132        return this.renderAsPercentages;   
133    }
134    
135    /**
136     * Sets the flag that controls whether the renderer displays each item
137     * value as a percentage (so that the stacked bars add to 100%), and sends
138     * a {@link RendererChangeEvent} to all registered listeners.
139     * 
140     * @param asPercentages  the flag.
141     * 
142     * @see #getRenderAsPercentages()
143     * 
144     * @since 1.0.5
145     */
146    public void setRenderAsPercentages(boolean asPercentages) {
147        this.renderAsPercentages = asPercentages; 
148        notifyListeners(new RendererChangeEvent(this));
149    }
150
151    /**
152     * Returns <code>2</code> to indicate that this renderer requires two 
153     * passes for drawing (item labels are drawn in the second pass so that 
154     * they always appear in front of all the bars).
155     * 
156     * @return <code>2</code>.
157     */
158    public int getPassCount() {
159        return 2;
160    }
161
162    /**
163     * Initialises the renderer and returns a state object that should be 
164     * passed to all subsequent calls to the drawItem() method. Here there is 
165     * nothing to do.
166     *
167     * @param g2  the graphics device.
168     * @param dataArea  the area inside the axes.
169     * @param plot  the plot.
170     * @param data  the data.
171     * @param info  an optional info collection object to return data back to
172     *              the caller.
173     *
174     * @return A state object.
175     */
176    public XYItemRendererState initialise(Graphics2D g2,
177                                          Rectangle2D dataArea,
178                                          XYPlot plot,
179                                          XYDataset data,
180                                          PlotRenderingInfo info) {
181        return new XYBarRendererState(info);
182    }
183
184    /**
185     * Returns the range of values the renderer requires to display all the 
186     * items from the specified dataset.
187     * 
188     * @param dataset  the dataset (<code>null</code> permitted).
189     * 
190     * @return The range (<code>null</code> if the dataset is <code>null</code>
191     *         or empty).
192     */
193    public Range findRangeBounds(XYDataset dataset) {
194        if (dataset != null) {
195            if (this.renderAsPercentages) {
196                return new Range(0.0, 1.0);
197            }
198            else {
199                return DatasetUtilities.findStackedRangeBounds(
200                        (TableXYDataset) dataset);
201            }
202        }
203        else {
204            return null;
205        }
206    }
207
208    /**
209     * Draws the visual representation of a single data item.
210     *
211     * @param g2  the graphics device.
212     * @param state  the renderer state.
213     * @param dataArea  the area within which the plot is being drawn.
214     * @param info  collects information about the drawing.
215     * @param plot  the plot (can be used to obtain standard color information 
216     *              etc).
217     * @param domainAxis  the domain axis.
218     * @param rangeAxis  the range axis.
219     * @param dataset  the dataset.
220     * @param series  the series index (zero-based).
221     * @param item  the item index (zero-based).
222     * @param crosshairState  crosshair information for the plot 
223     *                        (<code>null</code> permitted).
224     * @param pass  the pass index.
225     */
226    public void drawItem(Graphics2D g2, 
227                         XYItemRendererState state,
228                         Rectangle2D dataArea,
229                         PlotRenderingInfo info,
230                         XYPlot plot,
231                         ValueAxis domainAxis,
232                         ValueAxis rangeAxis,
233                         XYDataset dataset,
234                         int series,
235                         int item,
236                         CrosshairState crosshairState,
237                         int pass) {
238        
239        if (!(dataset instanceof IntervalXYDataset 
240                && dataset instanceof TableXYDataset)) {
241            String message = "dataset (type " + dataset.getClass().getName() 
242                + ") has wrong type:";
243            boolean and = false;
244            if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) {
245                message += " it is no IntervalXYDataset";
246                and = true;
247            }
248            if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) {
249                if (and) {
250                    message += " and";
251                }
252                message += " it is no TableXYDataset";
253            }
254
255            throw new IllegalArgumentException(message);
256        }
257
258        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
259        double value = intervalDataset.getYValue(series, item);
260        if (Double.isNaN(value)) {
261            return;
262        }
263        
264        // if we are rendering the values as percentages, we need to calculate
265        // the total for the current item.  Unfortunately here we end up 
266        // repeating the calculation more times than is strictly necessary -
267        // hopefully I'll come back to this and find a way to add the 
268        // total(s) to the renderer state.  The other problem is we implicitly
269        // assume the dataset has no negative values...perhaps that can be
270        // fixed too.
271        double total = 0.0;  
272        if (this.renderAsPercentages) {
273            total = DatasetUtilities.calculateStackTotal(
274                    (TableXYDataset) dataset, item);
275            value = value / total;
276        }
277        
278        double positiveBase = 0.0;
279        double negativeBase = 0.0;
280        
281        for (int i = 0; i < series; i++) {
282            double v = dataset.getYValue(i, item);
283            if (!Double.isNaN(v)) {
284                if (this.renderAsPercentages) {
285                    v = v / total;
286                }
287                if (v > 0) {
288                    positiveBase = positiveBase + v;
289                }
290                else {
291                    negativeBase = negativeBase + v;
292                }
293            }
294        }
295
296        double translatedBase;
297        double translatedValue;
298        RectangleEdge edgeR = plot.getRangeAxisEdge();
299        if (value > 0.0) {
300            translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 
301                    edgeR);
302            translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 
303                    dataArea, edgeR);
304        }
305        else {
306            translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 
307                    edgeR);
308            translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 
309                    dataArea, edgeR);
310        }
311
312        RectangleEdge edgeD = plot.getDomainAxisEdge();
313        double startX = intervalDataset.getStartXValue(series, item);
314        if (Double.isNaN(startX)) {
315            return;
316        }
317        double translatedStartX = domainAxis.valueToJava2D(startX, dataArea, 
318                edgeD);
319
320        double endX = intervalDataset.getEndXValue(series, item);
321        if (Double.isNaN(endX)) {
322            return;
323        }
324        double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD);
325
326        double translatedWidth = Math.max(1, Math.abs(translatedEndX 
327                - translatedStartX));
328        double translatedHeight = Math.abs(translatedValue - translatedBase);
329        if (getMargin() > 0.0) {
330            double cut = translatedWidth * getMargin();
331            translatedWidth = translatedWidth - cut;
332            translatedStartX = translatedStartX + cut / 2;
333        }
334
335        Rectangle2D bar = null;
336        PlotOrientation orientation = plot.getOrientation();
337        if (orientation == PlotOrientation.HORIZONTAL) {
338            bar = new Rectangle2D.Double(Math.min(translatedBase, 
339                    translatedValue), translatedEndX, translatedHeight,
340                    translatedWidth);
341        }
342        else if (orientation == PlotOrientation.VERTICAL) {
343            bar = new Rectangle2D.Double(translatedStartX,
344                    Math.min(translatedBase, translatedValue),
345                    translatedWidth, translatedHeight);
346        }
347
348        if (pass == 0) {
349            Paint itemPaint = getItemPaint(series, item);
350            if (getGradientPaintTransformer() 
351                    != null && itemPaint instanceof GradientPaint) {
352                GradientPaint gp = (GradientPaint) itemPaint;
353                itemPaint = getGradientPaintTransformer().transform(gp, bar);
354            }
355            g2.setPaint(itemPaint);
356            g2.fill(bar);
357            if (isDrawBarOutline() 
358                    && Math.abs(translatedEndX - translatedStartX) > 3) {
359                g2.setStroke(getItemStroke(series, item));
360                g2.setPaint(getItemOutlinePaint(series, item));
361                g2.draw(bar);
362            }
363
364            // add an entity for the item...
365            if (info != null) {
366                EntityCollection entities = info.getOwner()
367                        .getEntityCollection();
368                if (entities != null) {
369                    addEntity(entities, bar, dataset, series, item, 
370                            bar.getCenterX(), bar.getCenterY());
371                }
372            }
373        }
374        else if (pass == 1) {
375            // handle item label drawing, now that we know all the bars have
376            // been drawn...
377            if (isItemLabelVisible(series, item)) {
378                XYItemLabelGenerator generator = getItemLabelGenerator(series, 
379                        item);
380                drawItemLabel(g2, dataset, series, item, plot, generator, bar, 
381                        value < 0.0);
382            }
383        }
384
385    }
386    
387    /**
388     * Tests this renderer for equality with an arbitrary object.
389     * 
390     * @param obj  the object (<code>null</code> permitted).
391     * 
392     * @return A boolean.
393     */
394    public boolean equals(Object obj) {
395        if (obj == this) {
396            return true;   
397        }
398        if (!(obj instanceof StackedXYBarRenderer)) {
399            return false;   
400        }
401        StackedXYBarRenderer that = (StackedXYBarRenderer) obj;
402        if (this.renderAsPercentages != that.renderAsPercentages) {
403            return false;   
404        }
405        return super.equals(obj);
406    }
407    
408    /**
409     * Returns a hash code for this instance.
410     * 
411     * @return A hash code.
412     */
413    public int hashCode() {
414        int result = super.hashCode();
415        result = result * 37 + (this.renderAsPercentages ? 1 : 0);
416        return result;
417    }
418    
419}