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 * StackedXYBarRenderer.java
029 * -------------------------
030 * (C) Copyright 2004-2008, 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 * 24-Jun-2008 : Added new barPainter mechanism (DG);
051 * 23-Sep-2008 : Check shadow visibility before drawing shadow (DG);
052 *
053 */
054
055package org.jfree.chart.renderer.xy;
056
057import java.awt.Graphics2D;
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 *
086 * The example shown here is generated by the
087 * <code>StackedXYBarChartDemo2.java</code> program included in the
088 * JFreeChart demo collection:
089 * <br><br>
090 * <img src="../../../../../images/StackedXYBarRendererSample.png"
091 * alt="StackedXYBarRendererSample.png" />
092
093 */
094public class StackedXYBarRenderer extends XYBarRenderer {
095
096    /** For serialization. */
097    private static final long serialVersionUID = -7049101055533436444L;
098
099    /** A flag that controls whether the bars display values or percentages. */
100    private boolean renderAsPercentages;
101
102    /**
103     * Creates a new renderer.
104     */
105    public StackedXYBarRenderer() {
106        this(0.0);
107    }
108
109    /**
110     * Creates a new renderer.
111     *
112     * @param margin  the percentual amount of the bars that are cut away.
113     */
114    public StackedXYBarRenderer(double margin) {
115        super(margin);
116        this.renderAsPercentages = false;
117
118        // set the default item label positions, which will only be used if
119        // the user requests visible item labels...
120        ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER,
121                TextAnchor.CENTER);
122        setBasePositiveItemLabelPosition(p);
123        setBaseNegativeItemLabelPosition(p);
124        setPositiveItemLabelPositionFallback(null);
125        setNegativeItemLabelPositionFallback(null);
126    }
127
128    /**
129     * Returns <code>true</code> if the renderer displays each item value as
130     * a percentage (so that the stacked bars add to 100%), and
131     * <code>false</code> otherwise.
132     *
133     * @return A boolean.
134     *
135     * @see #setRenderAsPercentages(boolean)
136     *
137     * @since 1.0.5
138     */
139    public boolean getRenderAsPercentages() {
140        return this.renderAsPercentages;
141    }
142
143    /**
144     * Sets the flag that controls whether the renderer displays each item
145     * value as a percentage (so that the stacked bars add to 100%), and sends
146     * a {@link RendererChangeEvent} to all registered listeners.
147     *
148     * @param asPercentages  the flag.
149     *
150     * @see #getRenderAsPercentages()
151     *
152     * @since 1.0.5
153     */
154    public void setRenderAsPercentages(boolean asPercentages) {
155        this.renderAsPercentages = asPercentages;
156        fireChangeEvent();
157    }
158
159    /**
160     * Returns <code>3</code> to indicate that this renderer requires three
161     * passes for drawing (shadows are drawn in the first pass, the bars in the
162     * second, and item labels are drawn in the third pass so that
163     * they always appear in front of all the bars).
164     *
165     * @return <code>2</code>.
166     */
167    public int getPassCount() {
168        return 3;
169    }
170
171    /**
172     * Initialises the renderer and returns a state object that should be
173     * passed to all subsequent calls to the drawItem() method. Here there is
174     * nothing to do.
175     *
176     * @param g2  the graphics device.
177     * @param dataArea  the area inside the axes.
178     * @param plot  the plot.
179     * @param data  the data.
180     * @param info  an optional info collection object to return data back to
181     *              the caller.
182     *
183     * @return A state object.
184     */
185    public XYItemRendererState initialise(Graphics2D g2,
186                                          Rectangle2D dataArea,
187                                          XYPlot plot,
188                                          XYDataset data,
189                                          PlotRenderingInfo info) {
190        return new XYBarRendererState(info);
191    }
192
193    /**
194     * Returns the range of values the renderer requires to display all the
195     * items from the specified dataset.
196     *
197     * @param dataset  the dataset (<code>null</code> permitted).
198     *
199     * @return The range (<code>null</code> if the dataset is <code>null</code>
200     *         or empty).
201     */
202    public Range findRangeBounds(XYDataset dataset) {
203        if (dataset != null) {
204            if (this.renderAsPercentages) {
205                return new Range(0.0, 1.0);
206            }
207            else {
208                return DatasetUtilities.findStackedRangeBounds(
209                        (TableXYDataset) dataset);
210            }
211        }
212        else {
213            return null;
214        }
215    }
216
217    /**
218     * Draws the visual representation of a single data item.
219     *
220     * @param g2  the graphics device.
221     * @param state  the renderer state.
222     * @param dataArea  the area within which the plot is being drawn.
223     * @param info  collects information about the drawing.
224     * @param plot  the plot (can be used to obtain standard color information
225     *              etc).
226     * @param domainAxis  the domain axis.
227     * @param rangeAxis  the range axis.
228     * @param dataset  the dataset.
229     * @param series  the series index (zero-based).
230     * @param item  the item index (zero-based).
231     * @param crosshairState  crosshair information for the plot
232     *                        (<code>null</code> permitted).
233     * @param pass  the pass index.
234     */
235    public void drawItem(Graphics2D g2,
236                         XYItemRendererState state,
237                         Rectangle2D dataArea,
238                         PlotRenderingInfo info,
239                         XYPlot plot,
240                         ValueAxis domainAxis,
241                         ValueAxis rangeAxis,
242                         XYDataset dataset,
243                         int series,
244                         int item,
245                         CrosshairState crosshairState,
246                         int pass) {
247
248        if (!(dataset instanceof IntervalXYDataset
249                && dataset instanceof TableXYDataset)) {
250            String message = "dataset (type " + dataset.getClass().getName()
251                + ") has wrong type:";
252            boolean and = false;
253            if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) {
254                message += " it is no IntervalXYDataset";
255                and = true;
256            }
257            if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) {
258                if (and) {
259                    message += " and";
260                }
261                message += " it is no TableXYDataset";
262            }
263
264            throw new IllegalArgumentException(message);
265        }
266
267        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
268        double value = intervalDataset.getYValue(series, item);
269        if (Double.isNaN(value)) {
270            return;
271        }
272
273        // if we are rendering the values as percentages, we need to calculate
274        // the total for the current item.  Unfortunately here we end up
275        // repeating the calculation more times than is strictly necessary -
276        // hopefully I'll come back to this and find a way to add the
277        // total(s) to the renderer state.  The other problem is we implicitly
278        // assume the dataset has no negative values...perhaps that can be
279        // fixed too.
280        double total = 0.0;
281        if (this.renderAsPercentages) {
282            total = DatasetUtilities.calculateStackTotal(
283                    (TableXYDataset) dataset, item);
284            value = value / total;
285        }
286
287        double positiveBase = 0.0;
288        double negativeBase = 0.0;
289
290        for (int i = 0; i < series; i++) {
291            double v = dataset.getYValue(i, item);
292            if (!Double.isNaN(v)) {
293                if (this.renderAsPercentages) {
294                    v = v / total;
295                }
296                if (v > 0) {
297                    positiveBase = positiveBase + v;
298                }
299                else {
300                    negativeBase = negativeBase + v;
301                }
302            }
303        }
304
305        double translatedBase;
306        double translatedValue;
307        RectangleEdge edgeR = plot.getRangeAxisEdge();
308        if (value > 0.0) {
309            translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
310                    edgeR);
311            translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
312                    dataArea, edgeR);
313        }
314        else {
315            translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
316                    edgeR);
317            translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
318                    dataArea, edgeR);
319        }
320
321        RectangleEdge edgeD = plot.getDomainAxisEdge();
322        double startX = intervalDataset.getStartXValue(series, item);
323        if (Double.isNaN(startX)) {
324            return;
325        }
326        double translatedStartX = domainAxis.valueToJava2D(startX, dataArea,
327                edgeD);
328
329        double endX = intervalDataset.getEndXValue(series, item);
330        if (Double.isNaN(endX)) {
331            return;
332        }
333        double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD);
334
335        double translatedWidth = Math.max(1, Math.abs(translatedEndX
336                - translatedStartX));
337        double translatedHeight = Math.abs(translatedValue - translatedBase);
338        if (getMargin() > 0.0) {
339            double cut = translatedWidth * getMargin();
340            translatedWidth = translatedWidth - cut;
341            translatedStartX = translatedStartX + cut / 2;
342        }
343
344        Rectangle2D bar = null;
345        PlotOrientation orientation = plot.getOrientation();
346        if (orientation == PlotOrientation.HORIZONTAL) {
347            bar = new Rectangle2D.Double(Math.min(translatedBase,
348                    translatedValue), translatedEndX, translatedHeight,
349                    translatedWidth);
350        }
351        else if (orientation == PlotOrientation.VERTICAL) {
352            bar = new Rectangle2D.Double(translatedStartX,
353                    Math.min(translatedBase, translatedValue),
354                    translatedWidth, translatedHeight);
355        }
356        boolean positive = (value > 0.0);
357        boolean inverted = rangeAxis.isInverted();
358        RectangleEdge barBase;
359        if (orientation == PlotOrientation.HORIZONTAL) {
360            if (positive && inverted || !positive && !inverted) {
361                barBase = RectangleEdge.RIGHT;
362            }
363            else {
364                barBase = RectangleEdge.LEFT;
365            }
366        }
367        else {
368            if (positive && !inverted || !positive && inverted) {
369                barBase = RectangleEdge.BOTTOM;
370            }
371            else {
372                barBase = RectangleEdge.TOP;
373            }
374        }
375
376        if (pass == 0) {
377            if (getShadowsVisible()) {
378                getBarPainter().paintBarShadow(g2, this, series, item, bar,
379                        barBase, false);
380            }
381        }
382        else if (pass == 1) {
383            getBarPainter().paintBar(g2, this, series, item, bar, barBase);
384
385            // add an entity for the item...
386            if (info != null) {
387                EntityCollection entities = info.getOwner()
388                        .getEntityCollection();
389                if (entities != null) {
390                    addEntity(entities, bar, dataset, series, item,
391                            bar.getCenterX(), bar.getCenterY());
392                }
393            }
394        }
395        else if (pass == 2) {
396            // handle item label drawing, now that we know all the bars have
397            // been drawn...
398            if (isItemLabelVisible(series, item)) {
399                XYItemLabelGenerator generator = getItemLabelGenerator(series,
400                        item);
401                drawItemLabel(g2, dataset, series, item, plot, generator, bar,
402                        value < 0.0);
403            }
404        }
405
406    }
407
408    /**
409     * Tests this renderer for equality with an arbitrary object.
410     *
411     * @param obj  the object (<code>null</code> permitted).
412     *
413     * @return A boolean.
414     */
415    public boolean equals(Object obj) {
416        if (obj == this) {
417            return true;
418        }
419        if (!(obj instanceof StackedXYBarRenderer)) {
420            return false;
421        }
422        StackedXYBarRenderer that = (StackedXYBarRenderer) obj;
423        if (this.renderAsPercentages != that.renderAsPercentages) {
424            return false;
425        }
426        return super.equals(obj);
427    }
428
429    /**
430     * Returns a hash code for this instance.
431     *
432     * @return A hash code.
433     */
434    public int hashCode() {
435        int result = super.hashCode();
436        result = result * 37 + (this.renderAsPercentages ? 1 : 0);
437        return result;
438    }
439
440}