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