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