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