    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    }