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