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