001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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-2007, by Dan Rivett (d.rivett@ukonline.co.uk) and 031 * Contributors. 032 * 033 * Original Author: Dan Rivett (adapted from AreaCategoryItemRenderer); 034 * Contributor(s): Jon Iles; 035 * David Gilbert (for Object Refinery Limited); 036 * Christian W. Zuckschwerdt; 037 * 038 * Changes: 039 * -------- 040 * 20-Sep-2002 : Version 1, contributed by Dan Rivett; 041 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 042 * CategoryToolTipGenerator interface (DG); 043 * 01-Nov-2002 : Added tooltips (DG); 044 * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis 045 * for category spacing. Renamed StackedAreaCategoryItemRenderer 046 * --> StackedAreaRenderer (DG); 047 * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG); 048 * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG); 049 * 17-Jan-2003 : Moved plot classes to a separate package (DG); 050 * 25-Mar-2003 : Implemented Serializable (DG); 051 * 13-May-2003 : Modified to take into account the plot orientation (DG); 052 * 30-Jul-2003 : Modified entity constructor (CZ); 053 * 07-Oct-2003 : Added renderer state (DG); 054 * 29-Apr-2004 : Added getRangeExtent() override (DG); 055 * 05-Nov-2004 : Modified drawItem() signature (DG); 056 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG); 057 * ------------- JFREECHART 1.0.x --------------------------------------------- 058 * 11-Oct-2006 : Added support for rendering data values as percentages, 059 * and added a second pass for drawing item labels (DG); 060 * 061 */ 062 063 package org.jfree.chart.renderer.category; 064 065 import java.awt.Graphics2D; 066 import java.awt.Paint; 067 import java.awt.Shape; 068 import java.awt.geom.GeneralPath; 069 import java.awt.geom.Rectangle2D; 070 import java.io.Serializable; 071 072 import org.jfree.chart.axis.CategoryAxis; 073 import org.jfree.chart.axis.ValueAxis; 074 import org.jfree.chart.entity.EntityCollection; 075 import org.jfree.chart.event.RendererChangeEvent; 076 import org.jfree.chart.plot.CategoryPlot; 077 import org.jfree.data.DataUtilities; 078 import org.jfree.data.Range; 079 import org.jfree.data.category.CategoryDataset; 080 import org.jfree.data.general.DatasetUtilities; 081 import org.jfree.ui.RectangleEdge; 082 import org.jfree.util.PublicCloneable; 083 084 /** 085 * A renderer that draws stacked area charts for a 086 * {@link org.jfree.chart.plot.CategoryPlot}. 087 */ 088 public class StackedAreaRenderer extends AreaRenderer 089 implements Cloneable, PublicCloneable, 090 Serializable { 091 092 /** For serialization. */ 093 private static final long serialVersionUID = -3595635038460823663L; 094 095 /** A flag that controls whether the areas display values or percentages. */ 096 private boolean renderAsPercentages; 097 098 /** 099 * 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 fireChangeEvent(); 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 }