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 * GroupedStackedBarRenderer.java
029 * ------------------------------
030 * (C) Copyright 2004-present, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.chart.renderer.category;
038
039import java.awt.Graphics2D;
040import java.awt.geom.Rectangle2D;
041import java.io.Serializable;
042
043import org.jfree.chart.axis.CategoryAxis;
044import org.jfree.chart.axis.ValueAxis;
045import org.jfree.chart.entity.EntityCollection;
046import org.jfree.chart.event.RendererChangeEvent;
047import org.jfree.chart.labels.CategoryItemLabelGenerator;
048import org.jfree.chart.plot.CategoryPlot;
049import org.jfree.chart.plot.PlotOrientation;
050import org.jfree.chart.ui.RectangleEdge;
051import org.jfree.chart.util.Args;
052import org.jfree.chart.util.PublicCloneable;
053import org.jfree.data.KeyToGroupMap;
054import org.jfree.data.Range;
055import org.jfree.data.category.CategoryDataset;
056import org.jfree.data.general.DatasetUtils;
057
058/**
059 * A renderer that draws stacked bars within groups.  This will probably be
060 * merged with the {@link StackedBarRenderer} class at some point.  The example
061 * shown here is generated by the {@code StackedBarChartDemo4.java}
062 * program included in the JFreeChart Demo Collection:
063 * <br><br>
064 * <img src="doc-files/GroupedStackedBarRendererSample.png"
065 * alt="GroupedStackedBarRendererSample.png">
066 */
067public class GroupedStackedBarRenderer extends StackedBarRenderer
068        implements Cloneable, PublicCloneable, Serializable {
069
070    /** For serialization. */
071    private static final long serialVersionUID = -2725921399005922939L;
072
073    /** A map used to assign each series to a group. */
074    private KeyToGroupMap seriesToGroupMap;
075
076    /**
077     * Creates a new renderer.
078     */
079    public GroupedStackedBarRenderer() {
080        super();
081        this.seriesToGroupMap = new KeyToGroupMap();
082    }
083
084    /**
085     * Updates the map used to assign each series to a group, and sends a
086     * {@link RendererChangeEvent} to all registered listeners.
087     *
088     * @param map  the map ({@code null} not permitted).
089     */
090    public void setSeriesToGroupMap(KeyToGroupMap map) {
091        Args.nullNotPermitted(map, "map");
092        this.seriesToGroupMap = map;
093        fireChangeEvent();
094    }
095
096    /**
097     * Returns the range of values the renderer requires to display all the
098     * items from the specified dataset.
099     *
100     * @param dataset  the dataset ({@code null} permitted).
101     *
102     * @return The range (or {@code null} if the dataset is
103     *         {@code null} or empty).
104     */
105    @Override
106    public Range findRangeBounds(CategoryDataset dataset) {
107        if (dataset == null) {
108            return null;
109        }
110        Range r = DatasetUtils.findStackedRangeBounds(
111                dataset, this.seriesToGroupMap);
112        return r;
113    }
114
115    /**
116     * Calculates the bar width and stores it in the renderer state.  We
117     * override the method in the base class to take account of the
118     * series-to-group mapping.
119     *
120     * @param plot  the plot.
121     * @param dataArea  the data area.
122     * @param rendererIndex  the renderer index.
123     * @param state  the renderer state.
124     */
125    @Override
126    protected void calculateBarWidth(CategoryPlot plot, Rectangle2D dataArea,
127            int rendererIndex, CategoryItemRendererState state) {
128
129        // calculate the bar width
130        CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex);
131        CategoryDataset data = plot.getDataset(rendererIndex);
132        if (data != null) {
133            PlotOrientation orientation = plot.getOrientation();
134            double space = 0.0;
135            if (orientation == PlotOrientation.HORIZONTAL) {
136                space = dataArea.getHeight();
137            }
138            else if (orientation == PlotOrientation.VERTICAL) {
139                space = dataArea.getWidth();
140            }
141            double maxWidth = space * getMaximumBarWidth();
142            int groups = this.seriesToGroupMap.getGroupCount();
143            int categories = data.getColumnCount();
144            int columns = groups * categories;
145            double categoryMargin = 0.0;
146            double itemMargin = 0.0;
147            if (categories > 1) {
148                categoryMargin = xAxis.getCategoryMargin();
149            }
150            if (groups > 1) {
151                itemMargin = getItemMargin();
152            }
153
154            double used = space * (1 - xAxis.getLowerMargin()
155                                     - xAxis.getUpperMargin()
156                                     - categoryMargin - itemMargin);
157            if (columns > 0) {
158                state.setBarWidth(Math.min(used / columns, maxWidth));
159            }
160            else {
161                state.setBarWidth(Math.min(used, maxWidth));
162            }
163        }
164
165    }
166
167    /**
168     * Calculates the coordinate of the first "side" of a bar.  This will be
169     * the minimum x-coordinate for a vertical bar, and the minimum
170     * y-coordinate for a horizontal bar.
171     *
172     * @param plot  the plot.
173     * @param orientation  the plot orientation.
174     * @param dataArea  the data area.
175     * @param domainAxis  the domain axis.
176     * @param state  the renderer state (has the bar width precalculated).
177     * @param row  the row index.
178     * @param column  the column index.
179     *
180     * @return The coordinate.
181     */
182    @Override
183    protected double calculateBarW0(CategoryPlot plot, 
184            PlotOrientation orientation, Rectangle2D dataArea,
185            CategoryAxis domainAxis, CategoryItemRendererState state,
186            int row, int column) {
187        // calculate bar width...
188        double space;
189        if (orientation == PlotOrientation.HORIZONTAL) {
190            space = dataArea.getHeight();
191        }
192        else {
193            space = dataArea.getWidth();
194        }
195        double barW0 = domainAxis.getCategoryStart(column, getColumnCount(),
196                dataArea, plot.getDomainAxisEdge());
197        int groupCount = this.seriesToGroupMap.getGroupCount();
198        int groupIndex = this.seriesToGroupMap.getGroupIndex(
199                this.seriesToGroupMap.getGroup(plot.getDataset(
200                        plot.getIndexOf(this)).getRowKey(row)));
201        int categoryCount = getColumnCount();
202        if (groupCount > 1) {
203            double groupGap = space * getItemMargin()
204                              / (categoryCount * (groupCount - 1));
205            double groupW = calculateSeriesWidth(space, domainAxis,
206                    categoryCount, groupCount);
207            barW0 = barW0 + groupIndex * (groupW + groupGap)
208                          + (groupW / 2.0) - (state.getBarWidth() / 2.0);
209        }
210        else {
211            barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(),
212                    dataArea, plot.getDomainAxisEdge())
213                    - state.getBarWidth() / 2.0;
214        }
215        return barW0;
216    }
217
218    /**
219     * Draws a stacked bar for a specific item.
220     *
221     * @param g2  the graphics device.
222     * @param state  the renderer state.
223     * @param dataArea  the plot area.
224     * @param plot  the plot.
225     * @param domainAxis  the domain (category) axis.
226     * @param rangeAxis  the range (value) axis.
227     * @param dataset  the data.
228     * @param row  the row index (zero-based).
229     * @param column  the column index (zero-based).
230     * @param pass  the pass index.
231     */
232    @Override
233    public void drawItem(Graphics2D g2, CategoryItemRendererState state,
234            Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
235            ValueAxis rangeAxis, CategoryDataset dataset, int row,
236            int column, int pass) {
237
238        // nothing is drawn for null values...
239        Number dataValue = dataset.getValue(row, column);
240        if (dataValue == null) {
241            return;
242        }
243
244        double value = dataValue.doubleValue();
245        Comparable group = this.seriesToGroupMap.getGroup(
246                dataset.getRowKey(row));
247        PlotOrientation orientation = plot.getOrientation();
248        double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis,
249                state, row, column);
250
251        double positiveBase = 0.0;
252        double negativeBase = 0.0;
253
254        for (int i = 0; i < row; i++) {
255            if (group.equals(this.seriesToGroupMap.getGroup(
256                    dataset.getRowKey(i)))) {
257                Number v = dataset.getValue(i, column);
258                if (v != null) {
259                    double d = v.doubleValue();
260                    if (d > 0) {
261                        positiveBase = positiveBase + d;
262                    }
263                    else {
264                        negativeBase = negativeBase + d;
265                    }
266                }
267            }
268        }
269
270        double translatedBase;
271        double translatedValue;
272        boolean positive = (value > 0.0);
273        boolean inverted = rangeAxis.isInverted();
274        RectangleEdge barBase;
275        if (orientation == PlotOrientation.HORIZONTAL) {
276            if (positive && inverted || !positive && !inverted) {
277                barBase = RectangleEdge.RIGHT;
278            }
279            else {
280                barBase = RectangleEdge.LEFT;
281            }
282        }
283        else {
284            if (positive && !inverted || !positive && inverted) {
285                barBase = RectangleEdge.BOTTOM;
286            }
287            else {
288                barBase = RectangleEdge.TOP;
289            }
290        }
291        RectangleEdge location = plot.getRangeAxisEdge();
292        if (value > 0.0) {
293            translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
294                    location);
295            translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
296                    dataArea, location);
297        }
298        else {
299            translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
300                    location);
301            translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
302                    dataArea, location);
303        }
304        double barL0 = Math.min(translatedBase, translatedValue);
305        double barLength = Math.max(Math.abs(translatedValue - translatedBase),
306                getMinimumBarLength());
307
308        Rectangle2D bar;
309        if (orientation == PlotOrientation.HORIZONTAL) {
310            bar = new Rectangle2D.Double(barL0, barW0, barLength,
311                    state.getBarWidth());
312        }
313        else {
314            bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(),
315                    barLength);
316        }
317        getBarPainter().paintBar(g2, this, row, column, bar, barBase);
318
319        CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
320                column);
321        if (generator != null && isItemLabelVisible(row, column)) {
322            drawItemLabel(g2, dataset, row, column, plot, generator, bar,
323                    (value < 0.0));
324        }
325
326        // collect entity and tool tip information...
327        if (state.getInfo() != null) {
328            EntityCollection entities = state.getEntityCollection();
329            if (entities != null) {
330                addItemEntity(entities, dataset, row, column, bar);
331            }
332        }
333
334    }
335
336    /**
337     * Tests this renderer for equality with an arbitrary object.
338     *
339     * @param obj  the object ({@code null} permitted).
340     *
341     * @return A boolean.
342     */
343    @Override
344    public boolean equals(Object obj) {
345        if (obj == this) {
346            return true;
347        }
348        if (!(obj instanceof GroupedStackedBarRenderer)) {
349            return false;
350        }
351        GroupedStackedBarRenderer that = (GroupedStackedBarRenderer) obj;
352        if (!this.seriesToGroupMap.equals(that.seriesToGroupMap)) {
353            return false;
354        }
355        return super.equals(obj);
356    }
357
358}