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