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