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-2007, 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-2007, 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:  * Changes:
  39:  * --------
  40:  * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
  41:  * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 
  42:  *               CategoryToolTipGenerator interface (DG);
  43:  * 01-Nov-2002 : Added tooltips (DG);
  44:  * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis 
  45:  *               for category spacing. Renamed StackedAreaCategoryItemRenderer 
  46:  *               --> StackedAreaRenderer (DG);
  47:  * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
  48:  * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
  49:  * 17-Jan-2003 : Moved plot classes to a separate package (DG);
  50:  * 25-Mar-2003 : Implemented Serializable (DG);
  51:  * 13-May-2003 : Modified to take into account the plot orientation (DG);
  52:  * 30-Jul-2003 : Modified entity constructor (CZ);
  53:  * 07-Oct-2003 : Added renderer state (DG);
  54:  * 29-Apr-2004 : Added getRangeExtent() override (DG);
  55:  * 05-Nov-2004 : Modified drawItem() signature (DG);
  56:  * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
  57:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  58:  * 11-Oct-2006 : Added support for rendering data values as percentages,
  59:  *               and added a second pass for drawing item labels (DG);
  60:  * 
  61:  */
  62: 
  63: package org.jfree.chart.renderer.category;
  64: 
  65: import java.awt.Graphics2D;
  66: import java.awt.Paint;
  67: import java.awt.Shape;
  68: import java.awt.geom.GeneralPath;
  69: import java.awt.geom.Rectangle2D;
  70: import java.io.Serializable;
  71: 
  72: import org.jfree.chart.axis.CategoryAxis;
  73: import org.jfree.chart.axis.ValueAxis;
  74: import org.jfree.chart.entity.EntityCollection;
  75: import org.jfree.chart.event.RendererChangeEvent;
  76: import org.jfree.chart.plot.CategoryPlot;
  77: import org.jfree.data.DataUtilities;
  78: import org.jfree.data.Range;
  79: import org.jfree.data.category.CategoryDataset;
  80: import org.jfree.data.general.DatasetUtilities;
  81: import org.jfree.ui.RectangleEdge;
  82: import org.jfree.util.PublicCloneable;
  83: 
  84: /**
  85:  * A renderer that draws stacked area charts for a 
  86:  * {@link org.jfree.chart.plot.CategoryPlot}.
  87:  */
  88: public class StackedAreaRenderer extends AreaRenderer 
  89:                                  implements Cloneable, PublicCloneable, 
  90:                                             Serializable {
  91: 
  92:     /** For serialization. */
  93:     private static final long serialVersionUID = -3595635038460823663L;
  94:      
  95:     /** A flag that controls whether the areas display values or percentages. */
  96:     private boolean renderAsPercentages;
  97:     
  98:     /**
  99:      * Creates a new renderer.
 100:      */
 101:     public StackedAreaRenderer() {
 102:         this(false);
 103:     }
 104:     
 105:     /**
 106:      * Creates a new renderer.
 107:      * 
 108:      * @param renderAsPercentages  a flag that controls whether the data values
 109:      *                             are rendered as percentages.
 110:      */
 111:     public StackedAreaRenderer(boolean renderAsPercentages) {
 112:         super();
 113:         this.renderAsPercentages = renderAsPercentages;
 114:     }
 115: 
 116:     /**
 117:      * Returns <code>true</code> if the renderer displays each item value as
 118:      * a percentage (so that the stacked areas add to 100%), and 
 119:      * <code>false</code> otherwise.
 120:      * 
 121:      * @return A boolean.
 122:      *
 123:      * @since 1.0.3
 124:      */
 125:     public boolean getRenderAsPercentages() {
 126:         return this.renderAsPercentages;   
 127:     }
 128:     
 129:     /**
 130:      * Sets the flag that controls whether the renderer displays each item
 131:      * value as a percentage (so that the stacked areas add to 100%), and sends
 132:      * a {@link RendererChangeEvent} to all registered listeners.
 133:      * 
 134:      * @param asPercentages  the flag.
 135:      *
 136:      * @since 1.0.3
 137:      */
 138:     public void setRenderAsPercentages(boolean asPercentages) {
 139:         this.renderAsPercentages = asPercentages; 
 140:         notifyListeners(new RendererChangeEvent(this));
 141:     }
 142:     
 143:     /**
 144:      * Returns the number of passes (<code>2</code>) required by this renderer. 
 145:      * The first pass is used to draw the bars, the second pass is used to
 146:      * draw the item labels (if visible).
 147:      * 
 148:      * @return The number of passes required by the renderer.
 149:      */
 150:     public int getPassCount() {
 151:         return 2;
 152:     }
 153: 
 154:     /**
 155:      * Returns the range of values the renderer requires to display all the 
 156:      * items from the specified dataset.
 157:      * 
 158:      * @param dataset  the dataset (<code>null</code> not permitted).
 159:      * 
 160:      * @return The range (or <code>null</code> if the dataset is empty).
 161:      */
 162:     public Range findRangeBounds(CategoryDataset dataset) {
 163:         if (this.renderAsPercentages) {
 164:             return new Range(0.0, 1.0);   
 165:         }
 166:         else {
 167:             return DatasetUtilities.findStackedRangeBounds(dataset);
 168:         }
 169:     }
 170: 
 171:     /**
 172:      * Draw a single data item.
 173:      *
 174:      * @param g2  the graphics device.
 175:      * @param state  the renderer state.
 176:      * @param dataArea  the data plot area.
 177:      * @param plot  the plot.
 178:      * @param domainAxis  the domain axis.
 179:      * @param rangeAxis  the range axis.
 180:      * @param dataset  the data.
 181:      * @param row  the row index (zero-based).
 182:      * @param column  the column index (zero-based).
 183:      * @param pass  the pass index.
 184:      */
 185:     public void drawItem(Graphics2D g2,
 186:                          CategoryItemRendererState state,
 187:                          Rectangle2D dataArea,
 188:                          CategoryPlot plot,
 189:                          CategoryAxis domainAxis,
 190:                          ValueAxis rangeAxis,
 191:                          CategoryDataset dataset,
 192:                          int row,
 193:                          int column,
 194:                          int pass) {
 195: 
 196:         // setup for collecting optional entity info...
 197:         Shape entityArea = null;
 198:         EntityCollection entities = state.getEntityCollection();
 199:         
 200:         double y1 = 0.0;
 201:         Number n = dataset.getValue(row, column);
 202:         if (n != null) {
 203:             y1 = n.doubleValue();
 204:         }        
 205:         double[] stack1 = getStackValues(dataset, row, column);
 206: 
 207: 
 208:         // leave the y values (y1, y0) untranslated as it is going to be be 
 209:         // stacked up later by previous series values, after this it will be 
 210:         // translated.
 211:         double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
 212:                 dataArea, plot.getDomainAxisEdge());
 213:         
 214:         
 215:         // get the previous point and the next point so we can calculate a 
 216:         // "hot spot" for the area (used by the chart entity)...
 217:         double y0 = 0.0;
 218:         n = dataset.getValue(row, Math.max(column - 1, 0));
 219:         if (n != null) {
 220:             y0 = n.doubleValue();
 221:         }
 222:         double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0));
 223: 
 224:         // FIXME: calculate xx0
 225:         double xx0 = domainAxis.getCategoryStart(column, getColumnCount(), 
 226:                 dataArea, plot.getDomainAxisEdge());
 227:         
 228:         int itemCount = dataset.getColumnCount();
 229:         double y2 = 0.0;
 230:         n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
 231:         if (n != null) {
 232:             y2 = n.doubleValue();
 233:         }
 234:         double[] stack2 = getStackValues(dataset, row, Math.min(column + 1, 
 235:                 itemCount - 1));
 236: 
 237:         double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(), 
 238:                 dataArea, plot.getDomainAxisEdge());
 239:         
 240:         // FIXME: calculate xxLeft and xxRight
 241:         double xxLeft = xx0;
 242:         double xxRight = xx2;
 243:         
 244:         double[] stackLeft = averageStackValues(stack0, stack1);
 245:         double[] stackRight = averageStackValues(stack1, stack2);
 246:         double[] adjStackLeft = adjustedStackValues(stack0, stack1);
 247:         double[] adjStackRight = adjustedStackValues(stack1, stack2);
 248: 
 249:         float transY1;
 250:         
 251:         RectangleEdge edge1 = plot.getRangeAxisEdge();
 252:         
 253:         GeneralPath left = new GeneralPath();
 254:         GeneralPath right = new GeneralPath();
 255:         if (y1 >= 0.0) {  // handle positive value
 256:             transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, 
 257:                     edge1);
 258:             float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], 
 259:                     dataArea, edge1);
 260:             float transStackLeft = (float) rangeAxis.valueToJava2D(
 261:                     adjStackLeft[1], dataArea, edge1);
 262:             
 263:             // LEFT POLYGON
 264:             if (y0 >= 0.0) {
 265:                 double yleft = (y0 + y1) / 2.0 + stackLeft[1];
 266:                 float transYLeft 
 267:                     = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
 268:                 left.moveTo((float) xx1, transY1);
 269:                 left.lineTo((float) xx1, transStack1);
 270:                 left.lineTo((float) xxLeft, transStackLeft);
 271:                 left.lineTo((float) xxLeft, transYLeft);
 272:                 left.closePath();
 273:             }
 274:             else {
 275:                 left.moveTo((float) xx1, transStack1);
 276:                 left.lineTo((float) xx1, transY1);
 277:                 left.lineTo((float) xxLeft, transStackLeft);
 278:                 left.closePath();
 279:             }
 280: 
 281:             float transStackRight = (float) rangeAxis.valueToJava2D(
 282:                     adjStackRight[1], dataArea, edge1);
 283:             // RIGHT POLYGON
 284:             if (y2 >= 0.0) {
 285:                 double yright = (y1 + y2) / 2.0 + stackRight[1];
 286:                 float transYRight 
 287:                     = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
 288:                 right.moveTo((float) xx1, transStack1);
 289:                 right.lineTo((float) xx1, transY1);
 290:                 right.lineTo((float) xxRight, transYRight);
 291:                 right.lineTo((float) xxRight, transStackRight);
 292:                 right.closePath();
 293:             }
 294:             else {
 295:                 right.moveTo((float) xx1, transStack1);
 296:                 right.lineTo((float) xx1, transY1);
 297:                 right.lineTo((float) xxRight, transStackRight);
 298:                 right.closePath();
 299:             }
 300:         }
 301:         else {  // handle negative value 
 302:             transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
 303:                     edge1);
 304:             float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], 
 305:                     dataArea, edge1);
 306:             float transStackLeft = (float) rangeAxis.valueToJava2D(
 307:                     adjStackLeft[0], dataArea, edge1);
 308: 
 309:             // LEFT POLYGON
 310:             if (y0 >= 0.0) {
 311:                 left.moveTo((float) xx1, transStack1);
 312:                 left.lineTo((float) xx1, transY1);
 313:                 left.lineTo((float) xxLeft, transStackLeft);
 314:                 left.clone();
 315:             }
 316:             else {
 317:                 double yleft = (y0 + y1) / 2.0 + stackLeft[0];
 318:                 float transYLeft = (float) rangeAxis.valueToJava2D(yleft, 
 319:                         dataArea, edge1);
 320:                 left.moveTo((float) xx1, transY1);
 321:                 left.lineTo((float) xx1, transStack1);
 322:                 left.lineTo((float) xxLeft, transStackLeft);
 323:                 left.lineTo((float) xxLeft, transYLeft);
 324:                 left.closePath();
 325:             }
 326:             float transStackRight = (float) rangeAxis.valueToJava2D(
 327:                     adjStackRight[0], dataArea, edge1);
 328:             
 329:             // RIGHT POLYGON
 330:             if (y2 >= 0.0) {
 331:                 right.moveTo((float) xx1, transStack1);
 332:                 right.lineTo((float) xx1, transY1);
 333:                 right.lineTo((float) xxRight, transStackRight);
 334:                 right.closePath();
 335:             }
 336:             else {
 337:                 double yright = (y1 + y2) / 2.0 + stackRight[0];
 338:                 float transYRight = (float) rangeAxis.valueToJava2D(yright, 
 339:                         dataArea, edge1);
 340:                 right.moveTo((float) xx1, transStack1);
 341:                 right.lineTo((float) xx1, transY1);
 342:                 right.lineTo((float) xxRight, transYRight);
 343:                 right.lineTo((float) xxRight, transStackRight);
 344:                 right.closePath();
 345:             }
 346:         }
 347: 
 348:         g2.setPaint(getItemPaint(row, column));
 349:         g2.setStroke(getItemStroke(row, column));
 350: 
 351:         //  Get series Paint and Stroke
 352:         Paint itemPaint = getItemPaint(row, column);
 353:         if (pass == 0) {
 354:             g2.setPaint(itemPaint);
 355:             g2.fill(left);
 356:             g2.fill(right);
 357:         } 
 358:         
 359:         // add an entity for the item...
 360:         if (entities != null) {
 361:             GeneralPath gp = new GeneralPath(left);
 362:             gp.append(right, false);
 363:             entityArea = gp;
 364:             addItemEntity(entities, dataset, row, column, entityArea);
 365:         }
 366:         
 367:     }
 368: 
 369:     /**
 370:      * Calculates the stacked value of the all series up to, but not including 
 371:      * <code>series</code> for the specified category, <code>category</code>.  
 372:      * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
 373:      *
 374:      * @param dataset  the dataset (<code>null</code> not permitted).
 375:      * @param series  the series.
 376:      * @param category  the category.
 377:      *
 378:      * @return double returns a cumulative value for all series' values up to 
 379:      *         but excluding <code>series</code> for Object 
 380:      *         <code>category</code>.
 381:      */
 382:     protected double getPreviousHeight(CategoryDataset dataset, 
 383:                                        int series, int category) {
 384: 
 385:         double result = 0.0;
 386:         Number n;
 387:         double total = 0.0;
 388:         if (this.renderAsPercentages) {
 389:             total = DataUtilities.calculateColumnTotal(dataset, category);
 390:         }
 391:         for (int i = 0; i < series; i++) {
 392:             n = dataset.getValue(i, category);
 393:             if (n != null) {
 394:                 double v = n.doubleValue();
 395:                 if (this.renderAsPercentages) {
 396:                     v = v / total;
 397:                 }
 398:                 result += v;
 399:             }
 400:         }
 401:         return result;
 402: 
 403:     }
 404: 
 405:     /**
 406:      * Calculates the stacked values (one positive and one negative) of all 
 407:      * series up to, but not including, <code>series</code> for the specified 
 408:      * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
 409:      *
 410:      * @param dataset  the dataset (<code>null</code> not permitted).
 411:      * @param series  the series index.
 412:      * @param index  the item index.
 413:      *
 414:      * @return An array containing the cumulative negative and positive values
 415:      *     for all series values up to but excluding <code>series</code> 
 416:      *     for <code>index</code>.
 417:      */
 418:     protected double[] getStackValues(CategoryDataset dataset, 
 419:             int series, int index) {
 420:         double[] result = new double[2];
 421:         for (int i = 0; i < series; i++) {
 422:             if (isSeriesVisible(i)) {
 423:                 double v = 0.0;
 424:                 Number n = dataset.getValue(i, index);
 425:                 if (n != null) {
 426:                     v = n.doubleValue();
 427:                 }
 428:                 if (!Double.isNaN(v)) {
 429:                     if (v >= 0.0) {
 430:                         result[1] += v;   
 431:                     }
 432:                     else {
 433:                         result[0] += v;   
 434:                     }
 435:                 }
 436:             }
 437:         }
 438:         return result;
 439:     }
 440: 
 441:     /**
 442:      * Returns a pair of "stack" values calculated as the mean of the two 
 443:      * specified stack value pairs.
 444:      * 
 445:      * @param stack1  the first stack pair.
 446:      * @param stack2  the second stack pair.
 447:      * 
 448:      * @return A pair of average stack values.
 449:      */
 450:     private double[] averageStackValues(double[] stack1, double[] stack2) {
 451:         double[] result = new double[2];
 452:         result[0] = (stack1[0] + stack2[0]) / 2.0;
 453:         result[1] = (stack1[1] + stack2[1]) / 2.0;
 454:         return result;
 455:     }
 456: 
 457:     /**
 458:      * Calculates adjusted stack values from the supplied values.  The value is
 459:      * the mean of the supplied values, unless either of the supplied values
 460:      * is zero, in which case the adjusted value is zero also.
 461:      * 
 462:      * @param stack1  the first stack pair.
 463:      * @param stack2  the second stack pair.
 464:      * 
 465:      * @return A pair of average stack values.
 466:      */
 467:     private double[] adjustedStackValues(double[] stack1, double[] stack2) {
 468:         double[] result = new double[2];
 469:         if (stack1[0] == 0.0 || stack2[0] == 0.0) {
 470:             result[0] = 0.0;   
 471:         }
 472:         else {
 473:             result[0] = (stack1[0] + stack2[0]) / 2.0;
 474:         }
 475:         if (stack1[1] == 0.0 || stack2[1] == 0.0) {
 476:             result[1] = 0.0;   
 477:         }
 478:         else {
 479:             result[1] = (stack1[1] + stack2[1]) / 2.0;
 480:         }
 481:         return result;
 482:     }
 483: 
 484:     /**
 485:      * Checks this instance for equality with an arbitrary object.
 486:      *
 487:      * @param obj  the object (<code>null</code> not permitted).
 488:      *
 489:      * @return A boolean.
 490:      */
 491:     public boolean equals(Object obj) {
 492:         if (obj == this) {
 493:             return true;
 494:         }
 495:         if (!(obj instanceof StackedAreaRenderer)) {
 496:             return false;
 497:         }
 498:         StackedAreaRenderer that = (StackedAreaRenderer) obj;
 499:         if (this.renderAsPercentages != that.renderAsPercentages) {
 500:             return false;
 501:         }
 502:         return super.equals(obj);
 503:     }
 504: }