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