001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2020, 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-2020, by Andreas Schroeder and Contributors.
031 *
032 * Original Author:  Andreas Schroeder;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
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     * @since 1.0.5
119     */
120    public boolean getRenderAsPercentages() {
121        return this.renderAsPercentages;
122    }
123
124    /**
125     * Sets the flag that controls whether the renderer displays each item
126     * value as a percentage (so that the stacked bars add to 100%), and sends
127     * a {@link RendererChangeEvent} to all registered listeners.
128     *
129     * @param asPercentages  the flag.
130     *
131     * @see #getRenderAsPercentages()
132     *
133     * @since 1.0.5
134     */
135    public void setRenderAsPercentages(boolean asPercentages) {
136        this.renderAsPercentages = asPercentages;
137        fireChangeEvent();
138    }
139
140    /**
141     * Returns {@code 3} to indicate that this renderer requires three
142     * passes for drawing (shadows are drawn in the first pass, the bars in the
143     * second, and item labels are drawn in the third pass so that
144     * they always appear in front of all the bars).
145     *
146     * @return {@code 2}.
147     */
148    @Override
149    public int getPassCount() {
150        return 3;
151    }
152
153    /**
154     * Initialises the renderer and returns a state object that should be
155     * passed to all subsequent calls to the drawItem() method. Here there is
156     * nothing to do.
157     *
158     * @param g2  the graphics device.
159     * @param dataArea  the area inside the axes.
160     * @param plot  the plot.
161     * @param data  the data.
162     * @param info  an optional info collection object to return data back to
163     *              the caller.
164     *
165     * @return A state object.
166     */
167    @Override
168    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
169            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
170        return new XYBarRendererState(info);
171    }
172
173    /**
174     * Returns the range of values the renderer requires to display all the
175     * items from the specified dataset.
176     *
177     * @param dataset  the dataset ({@code null} permitted).
178     *
179     * @return The range ({@code null} if the dataset is {@code null}
180     *         or empty).
181     */
182    @Override
183    public Range findRangeBounds(XYDataset dataset) {
184        if (dataset != null) {
185            if (this.renderAsPercentages) {
186                return new Range(0.0, 1.0);
187            }
188            else {
189                return DatasetUtils.findStackedRangeBounds(
190                        (TableXYDataset) dataset);
191            }
192        }
193        else {
194            return null;
195        }
196    }
197
198    /**
199     * Draws the visual representation of a single data item.
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 information
206     *              etc).
207     * @param domainAxis  the domain axis.
208     * @param rangeAxis  the range axis.
209     * @param dataset  the dataset.
210     * @param series  the series index (zero-based).
211     * @param item  the item index (zero-based).
212     * @param crosshairState  crosshair information for the plot
213     *                        ({@code null} permitted).
214     * @param pass  the pass index.
215     */
216    @Override
217    public void drawItem(Graphics2D g2, XYItemRendererState state,
218            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
219            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
220            int series, int item, CrosshairState crosshairState, int pass) {
221
222        if (!getItemVisible(series, item)) {
223            return;
224        }
225
226        if (!(dataset instanceof IntervalXYDataset
227                && dataset instanceof TableXYDataset)) {
228            String message = "dataset (type " + dataset.getClass().getName()
229                + ") has wrong type:";
230            boolean and = false;
231            if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) {
232                message += " it is no IntervalXYDataset";
233                and = true;
234            }
235            if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) {
236                if (and) {
237                    message += " and";
238                }
239                message += " it is no TableXYDataset";
240            }
241
242            throw new IllegalArgumentException(message);
243        }
244
245        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
246        double value = intervalDataset.getYValue(series, item);
247        if (Double.isNaN(value)) {
248            return;
249        }
250
251        // if we are rendering the values as percentages, we need to calculate
252        // the total for the current item.  Unfortunately here we end up
253        // repeating the calculation more times than is strictly necessary -
254        // hopefully I'll come back to this and find a way to add the
255        // total(s) to the renderer state.  The other problem is we implicitly
256        // assume the dataset has no negative values...perhaps that can be
257        // fixed too.
258        double total = 0.0;
259        if (this.renderAsPercentages) {
260            total = DatasetUtils.calculateStackTotal(
261                    (TableXYDataset) dataset, item);
262            value = value / total;
263        }
264
265        double positiveBase = 0.0;
266        double negativeBase = 0.0;
267
268        for (int i = 0; i < series; i++) {
269            double v = dataset.getYValue(i, item);
270            if (!Double.isNaN(v) && isSeriesVisible(i)) {
271                if (this.renderAsPercentages) {
272                    v = v / total;
273                }
274                if (v > 0) {
275                    positiveBase = positiveBase + v;
276                }
277                else {
278                    negativeBase = negativeBase + v;
279                }
280            }
281        }
282
283        double translatedBase;
284        double translatedValue;
285        RectangleEdge edgeR = plot.getRangeAxisEdge();
286        if (value > 0.0) {
287            translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
288                    edgeR);
289            translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
290                    dataArea, edgeR);
291        }
292        else {
293            translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
294                    edgeR);
295            translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
296                    dataArea, edgeR);
297        }
298
299        RectangleEdge edgeD = plot.getDomainAxisEdge();
300        double startX = intervalDataset.getStartXValue(series, item);
301        if (Double.isNaN(startX)) {
302            return;
303        }
304        double translatedStartX = domainAxis.valueToJava2D(startX, dataArea,
305                edgeD);
306
307        double endX = intervalDataset.getEndXValue(series, item);
308        if (Double.isNaN(endX)) {
309            return;
310        }
311        double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD);
312
313        double translatedWidth = Math.max(1, Math.abs(translatedEndX
314                - translatedStartX));
315        double translatedHeight = Math.abs(translatedValue - translatedBase);
316        if (getMargin() > 0.0) {
317            double cut = translatedWidth * getMargin();
318            translatedWidth = translatedWidth - cut;
319            translatedStartX = translatedStartX + cut / 2;
320        }
321
322        Rectangle2D bar = null;
323        PlotOrientation orientation = plot.getOrientation();
324        if (orientation == PlotOrientation.HORIZONTAL) {
325            bar = new Rectangle2D.Double(Math.min(translatedBase,
326                    translatedValue), Math.min(translatedEndX,
327                    translatedStartX), translatedHeight, translatedWidth);
328        }
329        else if (orientation == PlotOrientation.VERTICAL) {
330            bar = new Rectangle2D.Double(Math.min(translatedStartX,
331                    translatedEndX), Math.min(translatedBase, translatedValue),
332                    translatedWidth, translatedHeight);
333        } else {
334            throw new IllegalStateException();
335        }
336        boolean positive = (value > 0.0);
337        boolean inverted = rangeAxis.isInverted();
338        RectangleEdge barBase;
339        if (orientation == PlotOrientation.HORIZONTAL) {
340            if (positive && inverted || !positive && !inverted) {
341                barBase = RectangleEdge.RIGHT;
342            }
343            else {
344                barBase = RectangleEdge.LEFT;
345            }
346        }
347        else {
348            if (positive && !inverted || !positive && inverted) {
349                barBase = RectangleEdge.BOTTOM;
350            }
351            else {
352                barBase = RectangleEdge.TOP;
353            }
354        }
355
356        if (pass == 0) {
357            if (getShadowsVisible()) {
358                getBarPainter().paintBarShadow(g2, this, series, item, bar,
359                        barBase, false);
360            }
361        }
362        else if (pass == 1) {
363            getBarPainter().paintBar(g2, this, series, item, bar, barBase);
364
365            // add an entity for the item...
366            if (info != null) {
367                EntityCollection entities = info.getOwner()
368                        .getEntityCollection();
369                if (entities != null) {
370                    addEntity(entities, bar, dataset, series, item,
371                            bar.getCenterX(), bar.getCenterY());
372                }
373            }
374        }
375        else if (pass == 2) {
376            // handle item label drawing, now that we know all the bars have
377            // been drawn...
378            if (isItemLabelVisible(series, item)) {
379                XYItemLabelGenerator generator = getItemLabelGenerator(series,
380                        item);
381                drawItemLabel(g2, dataset, series, item, plot, generator, bar,
382                        value < 0.0);
383            }
384        }
385
386    }
387
388    /**
389     * Tests this renderer for equality with an arbitrary object.
390     *
391     * @param obj  the object ({@code null} permitted).
392     *
393     * @return A boolean.
394     */
395    @Override
396    public boolean equals(Object obj) {
397        if (obj == this) {
398            return true;
399        }
400        if (!(obj instanceof StackedXYBarRenderer)) {
401            return false;
402        }
403        StackedXYBarRenderer that = (StackedXYBarRenderer) obj;
404        if (this.renderAsPercentages != that.renderAsPercentages) {
405            return false;
406        }
407        return super.equals(obj);
408    }
409
410    /**
411     * Returns a hash code for this instance.
412     *
413     * @return A hash code.
414     */
415    @Override
416    public int hashCode() {
417        int result = super.hashCode();
418        result = result * 37 + (this.renderAsPercentages ? 1 : 0);
419        return result;
420    }
421
422}