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