Source for org.jfree.chart.renderer.category.GroupedStackedBarRenderer

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * ------------------------------
  28:  * GroupedStackedBarRenderer.java
  29:  * ------------------------------
  30:  * (C) Copyright 2004, 2005, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   -;
  34:  *
  35:  * $Id: GroupedStackedBarRenderer.java,v 1.7.2.3 2005/12/01 20:16:57 mungady Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 29-Apr-2004 : Version 1 (DG);
  40:  * 08-Jul-2004 : Added equals() method (DG);
  41:  * 05-Nov-2004 : Modified drawItem() signature (DG);
  42:  * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds (DG);
  43:  * 20-Apr-2005 : Renamed CategoryLabelGenerator 
  44:  *               --> CategoryItemLabelGenerator (DG);
  45:  * 22-Sep-2005 : Renamed getMaxBarWidth() --> getMaximumBarWidth() (DG);
  46:  * 
  47:  */
  48:  
  49: package org.jfree.chart.renderer.category;
  50: 
  51: import java.awt.GradientPaint;
  52: import java.awt.Graphics2D;
  53: import java.awt.Paint;
  54: import java.awt.geom.Rectangle2D;
  55: import java.io.Serializable;
  56: 
  57: import org.jfree.chart.axis.CategoryAxis;
  58: import org.jfree.chart.axis.ValueAxis;
  59: import org.jfree.chart.entity.CategoryItemEntity;
  60: import org.jfree.chart.entity.EntityCollection;
  61: import org.jfree.chart.event.RendererChangeEvent;
  62: import org.jfree.chart.labels.CategoryItemLabelGenerator;
  63: import org.jfree.chart.labels.CategoryToolTipGenerator;
  64: import org.jfree.chart.plot.CategoryPlot;
  65: import org.jfree.chart.plot.PlotOrientation;
  66: import org.jfree.data.KeyToGroupMap;
  67: import org.jfree.data.Range;
  68: import org.jfree.data.category.CategoryDataset;
  69: import org.jfree.data.general.DatasetUtilities;
  70: import org.jfree.ui.RectangleEdge;
  71: import org.jfree.util.PublicCloneable;
  72: 
  73: /**
  74:  * A renderer that draws stacked bars within groups.  This will probably be 
  75:  * merged with the {@link StackedBarRenderer} class at some point.
  76:  */
  77: public class GroupedStackedBarRenderer extends StackedBarRenderer 
  78:                                        implements Cloneable, PublicCloneable, 
  79:                                                   Serializable {
  80:             
  81:     /** For serialization. */
  82:     private static final long serialVersionUID = -2725921399005922939L;
  83:     
  84:     /** A map used to assign each series to a group. */
  85:     private KeyToGroupMap seriesToGroupMap;
  86:     
  87:     /**
  88:      * Creates a new renderer.
  89:      */
  90:     public GroupedStackedBarRenderer() {
  91:         super();
  92:         this.seriesToGroupMap = new KeyToGroupMap();
  93:     }
  94:     
  95:     /**
  96:      * Updates the map used to assign each series to a group.
  97:      * 
  98:      * @param map  the map (<code>null</code> not permitted).
  99:      */
 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: }