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

   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:  * StackedAreaRenderer.java
  29:  * ------------------------
  30:  * (C) Copyright 2002-2006, by Dan Rivett (d.rivett@ukonline.co.uk) and 
  31:  *                          Contributors.
  32:  *
  33:  * Original Author:  Dan Rivett (adapted from AreaCategoryItemRenderer);
  34:  * Contributor(s):   Jon Iles;
  35:  *                   David Gilbert (for Object Refinery Limited);
  36:  *                   Christian W. Zuckschwerdt;
  37:  *
  38:  * $Id: StackedAreaRenderer.java,v 1.6.2.3 2006/10/11 16:25:50 mungady Exp $
  39:  *
  40:  * Changes:
  41:  * --------
  42:  * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
  43:  * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 
  44:  *               CategoryToolTipGenerator interface (DG);
  45:  * 01-Nov-2002 : Added tooltips (DG);
  46:  * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis 
  47:  *               for category spacing. Renamed StackedAreaCategoryItemRenderer 
  48:  *               --> StackedAreaRenderer (DG);
  49:  * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
  50:  * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
  51:  * 17-Jan-2003 : Moved plot classes to a separate package (DG);
  52:  * 25-Mar-2003 : Implemented Serializable (DG);
  53:  * 13-May-2003 : Modified to take into account the plot orientation (DG);
  54:  * 30-Jul-2003 : Modified entity constructor (CZ);
  55:  * 07-Oct-2003 : Added renderer state (DG);
  56:  * 29-Apr-2004 : Added getRangeExtent() override (DG);
  57:  * 05-Nov-2004 : Modified drawItem() signature (DG);
  58:  * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
  59:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  60:  * 11-Oct-2006 : Added support for rendering data values as percentages,
  61:  *               and added a second pass for drawing item labels (DG);
  62:  * 
  63:  */
  64: 
  65: package org.jfree.chart.renderer.category;
  66: 
  67: import java.awt.Graphics2D;
  68: import java.awt.Polygon;
  69: import java.awt.Shape;
  70: import java.awt.geom.Rectangle2D;
  71: import java.io.Serializable;
  72: 
  73: import org.jfree.chart.axis.CategoryAxis;
  74: import org.jfree.chart.axis.ValueAxis;
  75: import org.jfree.chart.entity.EntityCollection;
  76: import org.jfree.chart.event.RendererChangeEvent;
  77: import org.jfree.chart.plot.CategoryPlot;
  78: import org.jfree.chart.plot.PlotOrientation;
  79: import org.jfree.data.DataUtilities;
  80: import org.jfree.data.Range;
  81: import org.jfree.data.category.CategoryDataset;
  82: import org.jfree.data.general.DatasetUtilities;
  83: import org.jfree.ui.RectangleEdge;
  84: import org.jfree.util.PublicCloneable;
  85: 
  86: /**
  87:  * A renderer that draws stacked area charts for a 
  88:  * {@link org.jfree.chart.plot.CategoryPlot}.
  89:  */
  90: public class StackedAreaRenderer extends AreaRenderer 
  91:                                  implements Cloneable, PublicCloneable, 
  92:                                             Serializable {
  93: 
  94:     /** For serialization. */
  95:     private static final long serialVersionUID = -3595635038460823663L;
  96:      
  97:     /** A flag that controls whether the areas display values or percentages. */
  98:     private boolean renderAsPercentages;
  99:     
 100:     /**
 101:      * Creates a new renderer.
 102:      */
 103:     public StackedAreaRenderer() {
 104:         this(false);
 105:     }
 106:     
 107:     /**
 108:      * Creates a new renderer.
 109:      * 
 110:      * @param renderAsPercentages  a flag that controls whether the data values
 111:      *                             are rendered as percentages.
 112:      */
 113:     public StackedAreaRenderer(boolean renderAsPercentages) {
 114:         super();
 115:         this.renderAsPercentages = renderAsPercentages;
 116:     }
 117: 
 118:     /**
 119:      * Returns <code>true</code> if the renderer displays each item value as
 120:      * a percentage (so that the stacked areas add to 100%), and 
 121:      * <code>false</code> otherwise.
 122:      * 
 123:      * @return A boolean.
 124:      *
 125:      * @since 1.0.3
 126:      */
 127:     public boolean getRenderAsPercentages() {
 128:         return this.renderAsPercentages;   
 129:     }
 130:     
 131:     /**
 132:      * Sets the flag that controls whether the renderer displays each item
 133:      * value as a percentage (so that the stacked areas add to 100%), and sends
 134:      * a {@link RendererChangeEvent} to all registered listeners.
 135:      * 
 136:      * @param asPercentages  the flag.
 137:      *
 138:      * @since 1.0.3
 139:      */
 140:     public void setRenderAsPercentages(boolean asPercentages) {
 141:         this.renderAsPercentages = asPercentages; 
 142:         notifyListeners(new RendererChangeEvent(this));
 143:     }
 144:     
 145:     /**
 146:      * Returns the number of passes (<code>2</code>) required by this renderer. 
 147:      * The first pass is used to draw the bars, the second pass is used to
 148:      * draw the item labels (if visible).
 149:      * 
 150:      * @return The number of passes required by the renderer.
 151:      */
 152:     public int getPassCount() {
 153:         return 2;
 154:     }
 155: 
 156:     /**
 157:      * Returns the range of values the renderer requires to display all the 
 158:      * items from the specified dataset.
 159:      * 
 160:      * @param dataset  the dataset (<code>null</code> not permitted).
 161:      * 
 162:      * @return The range (or <code>null</code> if the dataset is empty).
 163:      */
 164:     public Range findRangeBounds(CategoryDataset dataset) {
 165:         if (this.renderAsPercentages) {
 166:             return new Range(0.0, 1.0);   
 167:         }
 168:         else {
 169:             return DatasetUtilities.findStackedRangeBounds(dataset);
 170:         }
 171:     }
 172: 
 173:     /**
 174:      * Draw a single data item.
 175:      *
 176:      * @param g2  the graphics device.
 177:      * @param state  the renderer state.
 178:      * @param dataArea  the data plot area.
 179:      * @param plot  the plot.
 180:      * @param domainAxis  the domain axis.
 181:      * @param rangeAxis  the range axis.
 182:      * @param dataset  the data.
 183:      * @param row  the row index (zero-based).
 184:      * @param column  the column index (zero-based).
 185:      * @param pass  the pass index.
 186:      */
 187:     public void drawItem(Graphics2D g2,
 188:                          CategoryItemRendererState state,
 189:                          Rectangle2D dataArea,
 190:                          CategoryPlot plot,
 191:                          CategoryAxis domainAxis,
 192:                          ValueAxis rangeAxis,
 193:                          CategoryDataset dataset,
 194:                          int row,
 195:                          int column,
 196:                          int pass) {
 197: 
 198:         // plot non-null values...
 199:         Number dataValue = dataset.getValue(row, column);
 200:         if (dataValue == null) {
 201:             return;
 202:         }
 203: 
 204:         double value = dataValue.doubleValue();
 205:         double total = 0.0;  // only needed if calculating percentages
 206:         if (this.renderAsPercentages) {
 207:             total = DataUtilities.calculateColumnTotal(dataset, column);
 208:             value = value / total;
 209:         }
 210: 
 211:         // leave the y values (y1, y0) untranslated as it is going to be be 
 212:         // stacked up later by previous series values, after this it will be 
 213:         // translated.
 214:         double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
 215:                 dataArea, plot.getDomainAxisEdge());
 216:         
 217:         double previousHeightx1 = getPreviousHeight(dataset, row, column);
 218:         double y1 = value + previousHeightx1;
 219:         RectangleEdge location = plot.getRangeAxisEdge();
 220:         double yy1 = rangeAxis.valueToJava2D(y1, dataArea, location);
 221: 
 222:         g2.setPaint(getItemPaint(row, column));
 223:         g2.setStroke(getItemStroke(row, column));
 224: 
 225:         // in column zero, the only job to do is draw any visible item labels
 226:         // and this is done in the second pass...
 227:         if (column == 0) {
 228:             if (pass == 1) {
 229:                 // draw item labels, if visible
 230:                 if (isItemLabelVisible(row, column)) {
 231:                     drawItemLabel(g2, plot.getOrientation(), dataset, row, column, 
 232:                             xx1, yy1, (y1 < 0.0));
 233:                 }    
 234:             }
 235:         }
 236:         else {
 237:             Number previousValue = dataset.getValue(row, column - 1);
 238:             if (previousValue != null) {
 239: 
 240:                 double xx0 = domainAxis.getCategoryMiddle(column - 1, 
 241:                         getColumnCount(), dataArea, plot.getDomainAxisEdge());
 242:                 double y0 = previousValue.doubleValue();
 243:                 if (this.renderAsPercentages) {
 244:                     total = DataUtilities.calculateColumnTotal(dataset, 
 245:                             column - 1);
 246:                     y0 = y0 / total;
 247:                 }
 248:                
 249: 
 250:                 // Get the previous height, but this will be different for both
 251:                 // y0 and y1 as the previous series values could differ.
 252:                 double previousHeightx0 = getPreviousHeight(dataset, row, 
 253:                         column - 1);
 254: 
 255:                 // Now stack the current y values on top of the previous values.
 256:                 y0 += previousHeightx0;
 257: 
 258:                 // Now translate the previous heights
 259:                 double previousHeightxx0 = rangeAxis.valueToJava2D(
 260:                         previousHeightx0, dataArea, location);
 261:                 double previousHeightxx1 = rangeAxis.valueToJava2D(
 262:                         previousHeightx1, dataArea, location);
 263: 
 264:                 // Now translate the current y values.
 265:                 double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location);
 266: 
 267:                 if (pass == 0) {
 268:                     Polygon p = null;
 269:                     PlotOrientation orientation = plot.getOrientation();
 270:                     if (orientation == PlotOrientation.HORIZONTAL) {
 271:                         p = new Polygon();
 272:                         p.addPoint((int) yy0, (int) xx0);
 273:                         p.addPoint((int) yy1, (int) xx1);
 274:                         p.addPoint((int) previousHeightxx1, (int) xx1);
 275:                         p.addPoint((int) previousHeightxx0, (int) xx0);
 276:                     }
 277:                     else if (orientation == PlotOrientation.VERTICAL) {
 278:                         p = new Polygon();
 279:                         p.addPoint((int) xx0, (int) yy0);
 280:                         p.addPoint((int) xx1, (int) yy1);
 281:                         p.addPoint((int) xx1, (int) previousHeightxx1);
 282:                         p.addPoint((int) xx0, (int) previousHeightxx0);
 283:                     }
 284:                     g2.setPaint(getItemPaint(row, column));
 285:                     g2.setStroke(getItemStroke(row, column));
 286:                     g2.fill(p);
 287:                 }
 288:                 else {
 289:                     if (isItemLabelVisible(row, column)) {
 290:                         drawItemLabel(g2, plot.getOrientation(), dataset, row, 
 291:                                 column, xx1, yy1, (y1 < 0.0));
 292:                     }  
 293:                 }
 294:             }
 295:             
 296: 
 297:         }
 298:         
 299: 
 300:         // add an item entity, if this information is being collected
 301:         EntityCollection entities = state.getEntityCollection();
 302:         if (entities != null) {
 303:             Shape shape = new Rectangle2D.Double(xx1 - 3.0, yy1 - 3.0, 6.0, 6.0);
 304:             addItemEntity(entities, dataset, row, column, shape);
 305:         }
 306: 
 307:     }
 308: 
 309:     /**
 310:      * Calculates the stacked value of the all series up to, but not including 
 311:      * <code>series</code> for the specified category, <code>category</code>.  
 312:      * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
 313:      *
 314:      * @param dataset  the dataset (<code>null</code> not permitted).
 315:      * @param series  the series.
 316:      * @param category  the category.
 317:      *
 318:      * @return double returns a cumulative value for all series' values up to 
 319:      *         but excluding <code>series</code> for Object 
 320:      *         <code>category</code>.
 321:      */
 322:     protected double getPreviousHeight(CategoryDataset dataset, 
 323:                                        int series, int category) {
 324: 
 325:         double result = 0.0;
 326:         Number n;
 327:         double total = 0.0;
 328:         if (this.renderAsPercentages) {
 329:             total = DataUtilities.calculateColumnTotal(dataset, category);
 330:         }
 331:         for (int i = 0; i < series; i++) {
 332:             n = dataset.getValue(i, category);
 333:             if (n != null) {
 334:                 double v = n.doubleValue();
 335:                 if (this.renderAsPercentages) {
 336:                     v = v / total;
 337:                 }
 338:                 result += v;
 339:             }
 340:         }
 341:         return result;
 342: 
 343:     }
 344: 
 345:     /**
 346:      * Checks this instance for equality with an arbitrary object.
 347:      *
 348:      * @param obj  the object (<code>null</code> not permitted).
 349:      *
 350:      * @return A boolean.
 351:      */
 352:     public boolean equals(Object obj) {
 353:         if (obj == this) {
 354:             return true;
 355:         }
 356:         if (! (obj instanceof StackedAreaRenderer)) {
 357:             return false;
 358:         }
 359:         StackedAreaRenderer that = (StackedAreaRenderer) obj;
 360:         if (this.renderAsPercentages != that.renderAsPercentages) {
 361:             return false;
 362:         }
 363:         return super.equals(obj);
 364:     }
 365: }