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
049package org.jfree.chart.renderer.category;
050
051import java.awt.Graphics2D;
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.EntityCollection;
058import org.jfree.chart.event.RendererChangeEvent;
059import org.jfree.chart.labels.CategoryItemLabelGenerator;
060import org.jfree.chart.plot.CategoryPlot;
061import org.jfree.chart.plot.PlotOrientation;
062import org.jfree.data.KeyToGroupMap;
063import org.jfree.data.Range;
064import org.jfree.data.category.CategoryDataset;
065import org.jfree.data.general.DatasetUtilities;
066import org.jfree.ui.RectangleEdge;
067import 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 */
078public 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}