001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2009, 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 * StackedAreaRenderer.java 029 * ------------------------ 030 * (C) Copyright 2002-2009, by Dan Rivett (d.rivett@ukonline.co.uk) and 031 * Contributors. 032 * 033 * Original Author: Dan Rivett (adapted from AreaRenderer); 034 * Contributor(s): Jon Iles; 035 * David Gilbert (for Object Refinery Limited); 036 * Christian W. Zuckschwerdt; 037 * Peter Kolb (patch 2511330); 038 * 039 * Changes: 040 * -------- 041 * 20-Sep-2002 : Version 1, contributed by Dan Rivett; 042 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 043 * CategoryToolTipGenerator interface (DG); 044 * 01-Nov-2002 : Added tooltips (DG); 045 * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis 046 * for category spacing. Renamed StackedAreaCategoryItemRenderer 047 * --> StackedAreaRenderer (DG); 048 * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG); 049 * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG); 050 * 17-Jan-2003 : Moved plot classes to a separate package (DG); 051 * 25-Mar-2003 : Implemented Serializable (DG); 052 * 13-May-2003 : Modified to take into account the plot orientation (DG); 053 * 30-Jul-2003 : Modified entity constructor (CZ); 054 * 07-Oct-2003 : Added renderer state (DG); 055 * 29-Apr-2004 : Added getRangeExtent() override (DG); 056 * 05-Nov-2004 : Modified drawItem() signature (DG); 057 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG); 058 * ------------- JFREECHART 1.0.x --------------------------------------------- 059 * 11-Oct-2006 : Added support for rendering data values as percentages, 060 * and added a second pass for drawing item labels (DG); 061 * 04-Feb-2009 : Fixed support for hidden series, and bug in findRangeBounds() 062 * method for null dataset (PK/DG); 063 * 04-Feb-2009 : Added item label support, and generate entities only in first 064 * pass (DG); 065 * 04-Feb-2009 : Fixed bug for renderAsPercentages == true (DG); 066 * 067 */ 068 069 package org.jfree.chart.renderer.category; 070 071 import java.awt.Graphics2D; 072 import java.awt.Paint; 073 import java.awt.Shape; 074 import java.awt.geom.GeneralPath; 075 import java.awt.geom.Rectangle2D; 076 import java.io.Serializable; 077 078 import org.jfree.chart.axis.CategoryAxis; 079 import org.jfree.chart.axis.ValueAxis; 080 import org.jfree.chart.entity.EntityCollection; 081 import org.jfree.chart.event.RendererChangeEvent; 082 import org.jfree.chart.plot.CategoryPlot; 083 import org.jfree.data.DataUtilities; 084 import org.jfree.data.Range; 085 import org.jfree.data.category.CategoryDataset; 086 import org.jfree.data.general.DatasetUtilities; 087 import org.jfree.ui.RectangleEdge; 088 import org.jfree.util.PublicCloneable; 089 090 /** 091 * A renderer that draws stacked area charts for a {@link CategoryPlot}. 092 * The example shown here is generated by the 093 * <code>StackedAreaChartDemo1.java</code> program included in the 094 * JFreeChart Demo Collection: 095 * <br><br> 096 * <img src="../../../../../images/StackedAreaRendererSample.png" 097 * alt="StackedAreaRendererSample.png" /> 098 */ 099 public class StackedAreaRenderer extends AreaRenderer 100 implements Cloneable, PublicCloneable, Serializable { 101 102 /** For serialization. */ 103 private static final long serialVersionUID = -3595635038460823663L; 104 105 /** A flag that controls whether the areas display values or percentages. */ 106 private boolean renderAsPercentages; 107 108 /** 109 * Creates a new renderer. 110 */ 111 public StackedAreaRenderer() { 112 this(false); 113 } 114 115 /** 116 * Creates a new renderer. 117 * 118 * @param renderAsPercentages a flag that controls whether the data values 119 * are rendered as percentages. 120 */ 121 public StackedAreaRenderer(boolean renderAsPercentages) { 122 super(); 123 this.renderAsPercentages = renderAsPercentages; 124 } 125 126 /** 127 * Returns <code>true</code> if the renderer displays each item value as 128 * a percentage (so that the stacked areas add to 100%), and 129 * <code>false</code> otherwise. 130 * 131 * @return A boolean. 132 * 133 * @since 1.0.3 134 */ 135 public boolean getRenderAsPercentages() { 136 return this.renderAsPercentages; 137 } 138 139 /** 140 * Sets the flag that controls whether the renderer displays each item 141 * value as a percentage (so that the stacked areas add to 100%), and sends 142 * a {@link RendererChangeEvent} to all registered listeners. 143 * 144 * @param asPercentages the flag. 145 * 146 * @since 1.0.3 147 */ 148 public void setRenderAsPercentages(boolean asPercentages) { 149 this.renderAsPercentages = asPercentages; 150 fireChangeEvent(); 151 } 152 153 /** 154 * Returns the number of passes (<code>2</code>) required by this renderer. 155 * The first pass is used to draw the areas, the second pass is used to 156 * draw the item labels (if visible). 157 * 158 * @return The number of passes required by the renderer. 159 */ 160 public int getPassCount() { 161 return 2; 162 } 163 164 /** 165 * Returns the range of values the renderer requires to display all the 166 * items from the specified dataset. 167 * 168 * @param dataset the dataset (<code>null</code> not permitted). 169 * 170 * @return The range (or <code>null</code> if the dataset is empty). 171 */ 172 public Range findRangeBounds(CategoryDataset dataset) { 173 if (dataset == null) { 174 return null; 175 } 176 if (this.renderAsPercentages) { 177 return new Range(0.0, 1.0); 178 } 179 else { 180 return DatasetUtilities.findStackedRangeBounds(dataset); 181 } 182 } 183 184 /** 185 * Draw a single data item. 186 * 187 * @param g2 the graphics device. 188 * @param state the renderer state. 189 * @param dataArea the data plot area. 190 * @param plot the plot. 191 * @param domainAxis the domain axis. 192 * @param rangeAxis the range axis. 193 * @param dataset the data. 194 * @param row the row index (zero-based). 195 * @param column the column index (zero-based). 196 * @param pass the pass index. 197 */ 198 public void drawItem(Graphics2D g2, 199 CategoryItemRendererState state, 200 Rectangle2D dataArea, 201 CategoryPlot plot, 202 CategoryAxis domainAxis, 203 ValueAxis rangeAxis, 204 CategoryDataset dataset, 205 int row, 206 int column, 207 int pass) { 208 209 if (!isSeriesVisible(row)) { 210 return; 211 } 212 213 // setup for collecting optional entity info... 214 Shape entityArea = null; 215 EntityCollection entities = state.getEntityCollection(); 216 217 double y1 = 0.0; 218 Number n = dataset.getValue(row, column); 219 if (n != null) { 220 y1 = n.doubleValue(); 221 if (this.renderAsPercentages) { 222 double total = DataUtilities.calculateColumnTotal(dataset, 223 column, state.getVisibleSeriesArray()); 224 y1 = y1 / total; 225 } 226 } 227 double[] stack1 = getStackValues(dataset, row, column, 228 state.getVisibleSeriesArray()); 229 230 231 // leave the y values (y1, y0) untranslated as it is going to be be 232 // stacked up later by previous series values, after this it will be 233 // translated. 234 double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 235 dataArea, plot.getDomainAxisEdge()); 236 237 238 // get the previous point and the next point so we can calculate a 239 // "hot spot" for the area (used by the chart entity)... 240 double y0 = 0.0; 241 n = dataset.getValue(row, Math.max(column - 1, 0)); 242 if (n != null) { 243 y0 = n.doubleValue(); 244 if (this.renderAsPercentages) { 245 double total = DataUtilities.calculateColumnTotal(dataset, 246 Math.max(column - 1, 0), state.getVisibleSeriesArray()); 247 y0 = y0 / total; 248 } 249 } 250 double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0), 251 state.getVisibleSeriesArray()); 252 253 // FIXME: calculate xx0 254 double xx0 = domainAxis.getCategoryStart(column, getColumnCount(), 255 dataArea, plot.getDomainAxisEdge()); 256 257 int itemCount = dataset.getColumnCount(); 258 double y2 = 0.0; 259 n = dataset.getValue(row, Math.min(column + 1, itemCount - 1)); 260 if (n != null) { 261 y2 = n.doubleValue(); 262 if (this.renderAsPercentages) { 263 double total = DataUtilities.calculateColumnTotal(dataset, 264 Math.min(column + 1, itemCount - 1), 265 state.getVisibleSeriesArray()); 266 y2 = y2 / total; 267 } 268 } 269 double[] stack2 = getStackValues(dataset, row, Math.min(column + 1, 270 itemCount - 1), state.getVisibleSeriesArray()); 271 272 double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(), 273 dataArea, plot.getDomainAxisEdge()); 274 275 // FIXME: calculate xxLeft and xxRight 276 double xxLeft = xx0; 277 double xxRight = xx2; 278 279 double[] stackLeft = averageStackValues(stack0, stack1); 280 double[] stackRight = averageStackValues(stack1, stack2); 281 double[] adjStackLeft = adjustedStackValues(stack0, stack1); 282 double[] adjStackRight = adjustedStackValues(stack1, stack2); 283 284 float transY1; 285 286 RectangleEdge edge1 = plot.getRangeAxisEdge(); 287 288 GeneralPath left = new GeneralPath(); 289 GeneralPath right = new GeneralPath(); 290 if (y1 >= 0.0) { // handle positive value 291 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, 292 edge1); 293 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], 294 dataArea, edge1); 295 float transStackLeft = (float) rangeAxis.valueToJava2D( 296 adjStackLeft[1], dataArea, edge1); 297 298 // LEFT POLYGON 299 if (y0 >= 0.0) { 300 double yleft = (y0 + y1) / 2.0 + stackLeft[1]; 301 float transYLeft 302 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1); 303 left.moveTo((float) xx1, transY1); 304 left.lineTo((float) xx1, transStack1); 305 left.lineTo((float) xxLeft, transStackLeft); 306 left.lineTo((float) xxLeft, transYLeft); 307 left.closePath(); 308 } 309 else { 310 left.moveTo((float) xx1, transStack1); 311 left.lineTo((float) xx1, transY1); 312 left.lineTo((float) xxLeft, transStackLeft); 313 left.closePath(); 314 } 315 316 float transStackRight = (float) rangeAxis.valueToJava2D( 317 adjStackRight[1], dataArea, edge1); 318 // RIGHT POLYGON 319 if (y2 >= 0.0) { 320 double yright = (y1 + y2) / 2.0 + stackRight[1]; 321 float transYRight 322 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1); 323 right.moveTo((float) xx1, transStack1); 324 right.lineTo((float) xx1, transY1); 325 right.lineTo((float) xxRight, transYRight); 326 right.lineTo((float) xxRight, transStackRight); 327 right.closePath(); 328 } 329 else { 330 right.moveTo((float) xx1, transStack1); 331 right.lineTo((float) xx1, transY1); 332 right.lineTo((float) xxRight, transStackRight); 333 right.closePath(); 334 } 335 } 336 else { // handle negative value 337 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea, 338 edge1); 339 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], 340 dataArea, edge1); 341 float transStackLeft = (float) rangeAxis.valueToJava2D( 342 adjStackLeft[0], dataArea, edge1); 343 344 // LEFT POLYGON 345 if (y0 >= 0.0) { 346 left.moveTo((float) xx1, transStack1); 347 left.lineTo((float) xx1, transY1); 348 left.lineTo((float) xxLeft, transStackLeft); 349 left.clone(); 350 } 351 else { 352 double yleft = (y0 + y1) / 2.0 + stackLeft[0]; 353 float transYLeft = (float) rangeAxis.valueToJava2D(yleft, 354 dataArea, edge1); 355 left.moveTo((float) xx1, transY1); 356 left.lineTo((float) xx1, transStack1); 357 left.lineTo((float) xxLeft, transStackLeft); 358 left.lineTo((float) xxLeft, transYLeft); 359 left.closePath(); 360 } 361 float transStackRight = (float) rangeAxis.valueToJava2D( 362 adjStackRight[0], dataArea, edge1); 363 364 // RIGHT POLYGON 365 if (y2 >= 0.0) { 366 right.moveTo((float) xx1, transStack1); 367 right.lineTo((float) xx1, transY1); 368 right.lineTo((float) xxRight, transStackRight); 369 right.closePath(); 370 } 371 else { 372 double yright = (y1 + y2) / 2.0 + stackRight[0]; 373 float transYRight = (float) rangeAxis.valueToJava2D(yright, 374 dataArea, edge1); 375 right.moveTo((float) xx1, transStack1); 376 right.lineTo((float) xx1, transY1); 377 right.lineTo((float) xxRight, transYRight); 378 right.lineTo((float) xxRight, transStackRight); 379 right.closePath(); 380 } 381 } 382 383 if (pass == 0) { 384 Paint itemPaint = getItemPaint(row, column); 385 g2.setPaint(itemPaint); 386 g2.fill(left); 387 g2.fill(right); 388 389 // add an entity for the item... 390 if (entities != null) { 391 GeneralPath gp = new GeneralPath(left); 392 gp.append(right, false); 393 entityArea = gp; 394 addItemEntity(entities, dataset, row, column, entityArea); 395 } 396 } 397 else if (pass == 1) { 398 drawItemLabel(g2, plot.getOrientation(), dataset, row, column, 399 xx1, transY1, y1 < 0.0); 400 } 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, int[] validRows) { 419 double[] result = new double[2]; 420 double total = 0.0; 421 if (this.renderAsPercentages) { 422 total = DataUtilities.calculateColumnTotal(dataset, index, 423 validRows); 424 } 425 for (int i = 0; i < series; i++) { 426 if (isSeriesVisible(i)) { 427 double v = 0.0; 428 Number n = dataset.getValue(i, index); 429 if (n != null) { 430 v = n.doubleValue(); 431 if (this.renderAsPercentages) { 432 v = v / total; 433 } 434 } 435 if (!Double.isNaN(v)) { 436 if (v >= 0.0) { 437 result[1] += v; 438 } 439 else { 440 result[0] += v; 441 } 442 } 443 } 444 } 445 return result; 446 } 447 448 /** 449 * Returns a pair of "stack" values calculated as the mean of the two 450 * specified stack value pairs. 451 * 452 * @param stack1 the first stack pair. 453 * @param stack2 the second stack pair. 454 * 455 * @return A pair of average stack values. 456 */ 457 private double[] averageStackValues(double[] stack1, double[] stack2) { 458 double[] result = new double[2]; 459 result[0] = (stack1[0] + stack2[0]) / 2.0; 460 result[1] = (stack1[1] + stack2[1]) / 2.0; 461 return result; 462 } 463 464 /** 465 * Calculates adjusted stack values from the supplied values. The value is 466 * the mean of the supplied values, unless either of the supplied values 467 * is zero, in which case the adjusted value is zero also. 468 * 469 * @param stack1 the first stack pair. 470 * @param stack2 the second stack pair. 471 * 472 * @return A pair of average stack values. 473 */ 474 private double[] adjustedStackValues(double[] stack1, double[] stack2) { 475 double[] result = new double[2]; 476 if (stack1[0] == 0.0 || stack2[0] == 0.0) { 477 result[0] = 0.0; 478 } 479 else { 480 result[0] = (stack1[0] + stack2[0]) / 2.0; 481 } 482 if (stack1[1] == 0.0 || stack2[1] == 0.0) { 483 result[1] = 0.0; 484 } 485 else { 486 result[1] = (stack1[1] + stack2[1]) / 2.0; 487 } 488 return result; 489 } 490 491 /** 492 * Checks this instance for equality with an arbitrary object. 493 * 494 * @param obj the object (<code>null</code> not permitted). 495 * 496 * @return A boolean. 497 */ 498 public boolean equals(Object obj) { 499 if (obj == this) { 500 return true; 501 } 502 if (!(obj instanceof StackedAreaRenderer)) { 503 return false; 504 } 505 StackedAreaRenderer that = (StackedAreaRenderer) obj; 506 if (this.renderAsPercentages != that.renderAsPercentages) { 507 return false; 508 } 509 return super.equals(obj); 510 } 511 512 /** 513 * Calculates the stacked value of the all series up to, but not including 514 * <code>series</code> for the specified category, <code>category</code>. 515 * It returns 0.0 if <code>series</code> is the first series, i.e. 0. 516 * 517 * @param dataset the dataset (<code>null</code> not permitted). 518 * @param series the series. 519 * @param category the category. 520 * 521 * @return double returns a cumulative value for all series' values up to 522 * but excluding <code>series</code> for Object 523 * <code>category</code>. 524 * 525 * @deprecated As of 1.0.13, as the method is never used internally. 526 */ 527 protected double getPreviousHeight(CategoryDataset dataset, 528 int series, int category) { 529 530 double result = 0.0; 531 Number n; 532 double total = 0.0; 533 if (this.renderAsPercentages) { 534 total = DataUtilities.calculateColumnTotal(dataset, category); 535 } 536 for (int i = 0; i < series; i++) { 537 n = dataset.getValue(i, category); 538 if (n != null) { 539 double v = n.doubleValue(); 540 if (this.renderAsPercentages) { 541 v = v / total; 542 } 543 result += v; 544 } 545 } 546 return result; 547 548 } 549 550 }