001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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-2008, 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, Serializable { 090 091 /** For serialization. */ 092 private static final long serialVersionUID = -3595635038460823663L; 093 094 /** A flag that controls whether the areas display values or percentages. */ 095 private boolean renderAsPercentages; 096 097 /** 098 * Creates a new renderer. 099 */ 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 }