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