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