001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * -------------------------
028 * StackedXYBarRenderer.java
029 * -------------------------
030 * (C) Copyright 2004-2014, 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 * 28-May-2009 : Fixed bar positioning with inverted domain axis (DG);
053 * 07-Act-2011 : Fix for Bug #3035289: Patch #3035325 (MH);
054 */
055
056package org.jfree.chart.renderer.xy;
057
058import java.awt.Graphics2D;
059import java.awt.geom.Rectangle2D;
060
061import org.jfree.chart.axis.ValueAxis;
062import org.jfree.chart.entity.EntityCollection;
063import org.jfree.chart.event.RendererChangeEvent;
064import org.jfree.chart.labels.ItemLabelAnchor;
065import org.jfree.chart.labels.ItemLabelPosition;
066import org.jfree.chart.labels.XYItemLabelGenerator;
067import org.jfree.chart.plot.CrosshairState;
068import org.jfree.chart.plot.PlotOrientation;
069import org.jfree.chart.plot.PlotRenderingInfo;
070import org.jfree.chart.plot.XYPlot;
071import org.jfree.data.Range;
072import org.jfree.data.general.DatasetUtilities;
073import org.jfree.data.xy.IntervalXYDataset;
074import org.jfree.data.xy.TableXYDataset;
075import org.jfree.data.xy.XYDataset;
076import org.jfree.ui.RectangleEdge;
077import org.jfree.ui.TextAnchor;
078
079/**
080 * A bar renderer that displays the series items stacked.
081 * The dataset used together with this renderer must be a
082 * {@link org.jfree.data.xy.IntervalXYDataset} and a
083 * {@link org.jfree.data.xy.TableXYDataset}. For example, the
084 * dataset class {@link org.jfree.data.xy.CategoryTableXYDataset}
085 * implements both interfaces.
086 *
087 * The example shown here is generated by the
088 * <code>StackedXYBarChartDemo2.java</code> program included in the
089 * JFreeChart demo collection:
090 * <br><br>
091 * <img src="../../../../../images/StackedXYBarRendererSample.png"
092 * alt="StackedXYBarRendererSample.png">
093
094 */
095public class StackedXYBarRenderer extends XYBarRenderer {
096
097    /** For serialization. */
098    private static final long serialVersionUID = -7049101055533436444L;
099
100    /** A flag that controls whether the bars display values or percentages. */
101    private boolean renderAsPercentages;
102
103    /**
104     * Creates a new renderer.
105     */
106    public StackedXYBarRenderer() {
107        this(0.0);
108    }
109
110    /**
111     * Creates a new renderer.
112     *
113     * @param margin  the percentual amount of the bars that are cut away.
114     */
115    public StackedXYBarRenderer(double margin) {
116        super(margin);
117        this.renderAsPercentages = false;
118
119        // set the default item label positions, which will only be used if
120        // the user requests visible item labels...
121        ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER,
122                TextAnchor.CENTER);
123        setBasePositiveItemLabelPosition(p);
124        setBaseNegativeItemLabelPosition(p);
125        setPositiveItemLabelPositionFallback(null);
126        setNegativeItemLabelPositionFallback(null);
127    }
128
129    /**
130     * Returns <code>true</code> if the renderer displays each item value as
131     * a percentage (so that the stacked bars add to 100%), and
132     * <code>false</code> otherwise.
133     *
134     * @return A boolean.
135     *
136     * @see #setRenderAsPercentages(boolean)
137     *
138     * @since 1.0.5
139     */
140    public boolean getRenderAsPercentages() {
141        return this.renderAsPercentages;
142    }
143
144    /**
145     * Sets the flag that controls whether the renderer displays each item
146     * value as a percentage (so that the stacked bars add to 100%), and sends
147     * a {@link RendererChangeEvent} to all registered listeners.
148     *
149     * @param asPercentages  the flag.
150     *
151     * @see #getRenderAsPercentages()
152     *
153     * @since 1.0.5
154     */
155    public void setRenderAsPercentages(boolean asPercentages) {
156        this.renderAsPercentages = asPercentages;
157        fireChangeEvent();
158    }
159
160    /**
161     * Returns <code>3</code> to indicate that this renderer requires three
162     * passes for drawing (shadows are drawn in the first pass, the bars in the
163     * second, and item labels are drawn in the third pass so that
164     * they always appear in front of all the bars).
165     *
166     * @return <code>2</code>.
167     */
168    @Override
169    public int getPassCount() {
170        return 3;
171    }
172
173    /**
174     * Initialises the renderer and returns a state object that should be
175     * passed to all subsequent calls to the drawItem() method. Here there is
176     * nothing to do.
177     *
178     * @param g2  the graphics device.
179     * @param dataArea  the area inside the axes.
180     * @param plot  the plot.
181     * @param data  the data.
182     * @param info  an optional info collection object to return data back to
183     *              the caller.
184     *
185     * @return A state object.
186     */
187    @Override
188    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
189            XYPlot plot, XYDataset data, 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    @Override
203    public Range findRangeBounds(XYDataset dataset) {
204        if (dataset != null) {
205            if (this.renderAsPercentages) {
206                return new Range(0.0, 1.0);
207            }
208            else {
209                return DatasetUtilities.findStackedRangeBounds(
210                        (TableXYDataset) dataset);
211            }
212        }
213        else {
214            return null;
215        }
216    }
217
218    /**
219     * Draws the visual representation of a single data item.
220     *
221     * @param g2  the graphics device.
222     * @param state  the renderer state.
223     * @param dataArea  the area within which the plot is being drawn.
224     * @param info  collects information about the drawing.
225     * @param plot  the plot (can be used to obtain standard color information
226     *              etc).
227     * @param domainAxis  the domain axis.
228     * @param rangeAxis  the range axis.
229     * @param dataset  the dataset.
230     * @param series  the series index (zero-based).
231     * @param item  the item index (zero-based).
232     * @param crosshairState  crosshair information for the plot
233     *                        (<code>null</code> permitted).
234     * @param pass  the pass index.
235     */
236    @Override
237    public void drawItem(Graphics2D g2, XYItemRendererState state,
238            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
239            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
240            int series, int item, CrosshairState crosshairState, int pass) {
241
242        if (!getItemVisible(series, item)) {
243            return;
244        }
245
246        if (!(dataset instanceof IntervalXYDataset
247                && dataset instanceof TableXYDataset)) {
248            String message = "dataset (type " + dataset.getClass().getName()
249                + ") has wrong type:";
250            boolean and = false;
251            if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) {
252                message += " it is no IntervalXYDataset";
253                and = true;
254            }
255            if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) {
256                if (and) {
257                    message += " and";
258                }
259                message += " it is no TableXYDataset";
260            }
261
262            throw new IllegalArgumentException(message);
263        }
264
265        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
266        double value = intervalDataset.getYValue(series, item);
267        if (Double.isNaN(value)) {
268            return;
269        }
270
271        // if we are rendering the values as percentages, we need to calculate
272        // the total for the current item.  Unfortunately here we end up
273        // repeating the calculation more times than is strictly necessary -
274        // hopefully I'll come back to this and find a way to add the
275        // total(s) to the renderer state.  The other problem is we implicitly
276        // assume the dataset has no negative values...perhaps that can be
277        // fixed too.
278        double total = 0.0;
279        if (this.renderAsPercentages) {
280            total = DatasetUtilities.calculateStackTotal(
281                    (TableXYDataset) dataset, item);
282            value = value / total;
283        }
284
285        double positiveBase = 0.0;
286        double negativeBase = 0.0;
287
288        for (int i = 0; i < series; i++) {
289            double v = dataset.getYValue(i, item);
290            if (!Double.isNaN(v) && isSeriesVisible(i)) {
291                if (this.renderAsPercentages) {
292                    v = v / total;
293                }
294                if (v > 0) {
295                    positiveBase = positiveBase + v;
296                }
297                else {
298                    negativeBase = negativeBase + v;
299                }
300            }
301        }
302
303        double translatedBase;
304        double translatedValue;
305        RectangleEdge edgeR = plot.getRangeAxisEdge();
306        if (value > 0.0) {
307            translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
308                    edgeR);
309            translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
310                    dataArea, edgeR);
311        }
312        else {
313            translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
314                    edgeR);
315            translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
316                    dataArea, edgeR);
317        }
318
319        RectangleEdge edgeD = plot.getDomainAxisEdge();
320        double startX = intervalDataset.getStartXValue(series, item);
321        if (Double.isNaN(startX)) {
322            return;
323        }
324        double translatedStartX = domainAxis.valueToJava2D(startX, dataArea,
325                edgeD);
326
327        double endX = intervalDataset.getEndXValue(series, item);
328        if (Double.isNaN(endX)) {
329            return;
330        }
331        double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD);
332
333        double translatedWidth = Math.max(1, Math.abs(translatedEndX
334                - translatedStartX));
335        double translatedHeight = Math.abs(translatedValue - translatedBase);
336        if (getMargin() > 0.0) {
337            double cut = translatedWidth * getMargin();
338            translatedWidth = translatedWidth - cut;
339            translatedStartX = translatedStartX + cut / 2;
340        }
341
342        Rectangle2D bar = null;
343        PlotOrientation orientation = plot.getOrientation();
344        if (orientation == PlotOrientation.HORIZONTAL) {
345            bar = new Rectangle2D.Double(Math.min(translatedBase,
346                    translatedValue), Math.min(translatedEndX,
347                    translatedStartX), translatedHeight, translatedWidth);
348        }
349        else if (orientation == PlotOrientation.VERTICAL) {
350            bar = new Rectangle2D.Double(Math.min(translatedStartX,
351                    translatedEndX), Math.min(translatedBase, translatedValue),
352                    translatedWidth, translatedHeight);
353        } else {
354            throw new IllegalStateException();
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    @Override
416    public boolean equals(Object obj) {
417        if (obj == this) {
418            return true;
419        }
420        if (!(obj instanceof StackedXYBarRenderer)) {
421            return false;
422        }
423        StackedXYBarRenderer that = (StackedXYBarRenderer) obj;
424        if (this.renderAsPercentages != that.renderAsPercentages) {
425            return false;
426        }
427        return super.equals(obj);
428    }
429
430    /**
431     * Returns a hash code for this instance.
432     *
433     * @return A hash code.
434     */
435    @Override
436    public int hashCode() {
437        int result = super.hashCode();
438        result = result * 37 + (this.renderAsPercentages ? 1 : 0);
439        return result;
440    }
441
442}