001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------------------ 028 * StackedAreaRenderer.java 029 * ------------------------ 030 * (C) Copyright 2002-2014, 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 069package org.jfree.chart.renderer.category; 070 071import java.awt.Graphics2D; 072import java.awt.Paint; 073import java.awt.Shape; 074import java.awt.geom.GeneralPath; 075import java.awt.geom.Rectangle2D; 076import java.io.Serializable; 077 078import org.jfree.chart.axis.CategoryAxis; 079import org.jfree.chart.axis.ValueAxis; 080import org.jfree.chart.entity.EntityCollection; 081import org.jfree.chart.event.RendererChangeEvent; 082import org.jfree.chart.plot.CategoryPlot; 083import org.jfree.data.DataUtilities; 084import org.jfree.data.Range; 085import org.jfree.data.category.CategoryDataset; 086import org.jfree.data.general.DatasetUtilities; 087import org.jfree.ui.RectangleEdge; 088import 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 */ 099public 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 @Override 161 public int getPassCount() { 162 return 2; 163 } 164 165 /** 166 * Returns the range of values the renderer requires to display all the 167 * items from the specified dataset. 168 * 169 * @param dataset the dataset (<code>null</code> not permitted). 170 * 171 * @return The range (or <code>null</code> if the dataset is empty). 172 */ 173 @Override 174 public Range findRangeBounds(CategoryDataset dataset) { 175 if (dataset == null) { 176 return null; 177 } 178 if (this.renderAsPercentages) { 179 return new Range(0.0, 1.0); 180 } 181 else { 182 return DatasetUtilities.findStackedRangeBounds(dataset); 183 } 184 } 185 186 /** 187 * Draw a single data item. 188 * 189 * @param g2 the graphics device. 190 * @param state the renderer state. 191 * @param dataArea the data plot area. 192 * @param plot the plot. 193 * @param domainAxis the domain axis. 194 * @param rangeAxis the range axis. 195 * @param dataset the data. 196 * @param row the row index (zero-based). 197 * @param column the column index (zero-based). 198 * @param pass the pass index. 199 */ 200 @Override 201 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 202 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 203 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 204 int pass) { 205 206 if (!isSeriesVisible(row)) { 207 return; 208 } 209 210 // setup for collecting optional entity info... 211 Shape entityArea; 212 EntityCollection entities = state.getEntityCollection(); 213 214 double y1 = 0.0; 215 Number n = dataset.getValue(row, column); 216 if (n != null) { 217 y1 = n.doubleValue(); 218 if (this.renderAsPercentages) { 219 double total = DataUtilities.calculateColumnTotal(dataset, 220 column, state.getVisibleSeriesArray()); 221 y1 = y1 / total; 222 } 223 } 224 double[] stack1 = getStackValues(dataset, row, column, 225 state.getVisibleSeriesArray()); 226 227 228 // leave the y values (y1, y0) untranslated as it is going to be be 229 // stacked up later by previous series values, after this it will be 230 // translated. 231 double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 232 dataArea, plot.getDomainAxisEdge()); 233 234 235 // get the previous point and the next point so we can calculate a 236 // "hot spot" for the area (used by the chart entity)... 237 double y0 = 0.0; 238 n = dataset.getValue(row, Math.max(column - 1, 0)); 239 if (n != null) { 240 y0 = n.doubleValue(); 241 if (this.renderAsPercentages) { 242 double total = DataUtilities.calculateColumnTotal(dataset, 243 Math.max(column - 1, 0), state.getVisibleSeriesArray()); 244 y0 = y0 / total; 245 } 246 } 247 double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0), 248 state.getVisibleSeriesArray()); 249 250 // FIXME: calculate xx0 251 double xx0 = domainAxis.getCategoryStart(column, getColumnCount(), 252 dataArea, plot.getDomainAxisEdge()); 253 254 int itemCount = dataset.getColumnCount(); 255 double y2 = 0.0; 256 n = dataset.getValue(row, Math.min(column + 1, itemCount - 1)); 257 if (n != null) { 258 y2 = n.doubleValue(); 259 if (this.renderAsPercentages) { 260 double total = DataUtilities.calculateColumnTotal(dataset, 261 Math.min(column + 1, itemCount - 1), 262 state.getVisibleSeriesArray()); 263 y2 = y2 / total; 264 } 265 } 266 double[] stack2 = getStackValues(dataset, row, Math.min(column + 1, 267 itemCount - 1), state.getVisibleSeriesArray()); 268 269 double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(), 270 dataArea, plot.getDomainAxisEdge()); 271 272 // FIXME: calculate xxLeft and xxRight 273 double xxLeft = xx0; 274 double xxRight = xx2; 275 276 double[] stackLeft = averageStackValues(stack0, stack1); 277 double[] stackRight = averageStackValues(stack1, stack2); 278 double[] adjStackLeft = adjustedStackValues(stack0, stack1); 279 double[] adjStackRight = adjustedStackValues(stack1, stack2); 280 281 float transY1; 282 283 RectangleEdge edge1 = plot.getRangeAxisEdge(); 284 285 GeneralPath left = new GeneralPath(); 286 GeneralPath right = new GeneralPath(); 287 if (y1 >= 0.0) { // handle positive value 288 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, 289 edge1); 290 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], 291 dataArea, edge1); 292 float transStackLeft = (float) rangeAxis.valueToJava2D( 293 adjStackLeft[1], dataArea, edge1); 294 295 // LEFT POLYGON 296 if (y0 >= 0.0) { 297 double yleft = (y0 + y1) / 2.0 + stackLeft[1]; 298 float transYLeft 299 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1); 300 left.moveTo((float) xx1, transY1); 301 left.lineTo((float) xx1, transStack1); 302 left.lineTo((float) xxLeft, transStackLeft); 303 left.lineTo((float) xxLeft, transYLeft); 304 left.closePath(); 305 } 306 else { 307 left.moveTo((float) xx1, transStack1); 308 left.lineTo((float) xx1, transY1); 309 left.lineTo((float) xxLeft, transStackLeft); 310 left.closePath(); 311 } 312 313 float transStackRight = (float) rangeAxis.valueToJava2D( 314 adjStackRight[1], dataArea, edge1); 315 // RIGHT POLYGON 316 if (y2 >= 0.0) { 317 double yright = (y1 + y2) / 2.0 + stackRight[1]; 318 float transYRight 319 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1); 320 right.moveTo((float) xx1, transStack1); 321 right.lineTo((float) xx1, transY1); 322 right.lineTo((float) xxRight, transYRight); 323 right.lineTo((float) xxRight, transStackRight); 324 right.closePath(); 325 } 326 else { 327 right.moveTo((float) xx1, transStack1); 328 right.lineTo((float) xx1, transY1); 329 right.lineTo((float) xxRight, transStackRight); 330 right.closePath(); 331 } 332 } 333 else { // handle negative value 334 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea, 335 edge1); 336 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], 337 dataArea, edge1); 338 float transStackLeft = (float) rangeAxis.valueToJava2D( 339 adjStackLeft[0], dataArea, edge1); 340 341 // LEFT POLYGON 342 if (y0 >= 0.0) { 343 left.moveTo((float) xx1, transStack1); 344 left.lineTo((float) xx1, transY1); 345 left.lineTo((float) xxLeft, transStackLeft); 346 left.clone(); 347 } 348 else { 349 double yleft = (y0 + y1) / 2.0 + stackLeft[0]; 350 float transYLeft = (float) rangeAxis.valueToJava2D(yleft, 351 dataArea, edge1); 352 left.moveTo((float) xx1, transY1); 353 left.lineTo((float) xx1, transStack1); 354 left.lineTo((float) xxLeft, transStackLeft); 355 left.lineTo((float) xxLeft, transYLeft); 356 left.closePath(); 357 } 358 float transStackRight = (float) rangeAxis.valueToJava2D( 359 adjStackRight[0], dataArea, edge1); 360 361 // RIGHT POLYGON 362 if (y2 >= 0.0) { 363 right.moveTo((float) xx1, transStack1); 364 right.lineTo((float) xx1, transY1); 365 right.lineTo((float) xxRight, transStackRight); 366 right.closePath(); 367 } 368 else { 369 double yright = (y1 + y2) / 2.0 + stackRight[0]; 370 float transYRight = (float) rangeAxis.valueToJava2D(yright, 371 dataArea, edge1); 372 right.moveTo((float) xx1, transStack1); 373 right.lineTo((float) xx1, transY1); 374 right.lineTo((float) xxRight, transYRight); 375 right.lineTo((float) xxRight, transStackRight); 376 right.closePath(); 377 } 378 } 379 380 if (pass == 0) { 381 Paint itemPaint = getItemPaint(row, column); 382 g2.setPaint(itemPaint); 383 g2.fill(left); 384 g2.fill(right); 385 386 // add an entity for the item... 387 if (entities != null) { 388 GeneralPath gp = new GeneralPath(left); 389 gp.append(right, false); 390 entityArea = gp; 391 addItemEntity(entities, dataset, row, column, entityArea); 392 } 393 } 394 else if (pass == 1) { 395 drawItemLabel(g2, plot.getOrientation(), dataset, row, column, 396 xx1, transY1, y1 < 0.0); 397 } 398 399 } 400 401 /** 402 * Calculates the stacked values (one positive and one negative) of all 403 * series up to, but not including, <code>series</code> for the specified 404 * item. It returns [0.0, 0.0] if <code>series</code> is the first series. 405 * 406 * @param dataset the dataset (<code>null</code> not permitted). 407 * @param series the series index. 408 * @param index the item index. 409 * @param validRows the valid rows. 410 * 411 * @return An array containing the cumulative negative and positive values 412 * for all series values up to but excluding <code>series</code> 413 * for <code>index</code>. 414 */ 415 protected double[] getStackValues(CategoryDataset dataset, 416 int series, int index, int[] validRows) { 417 double[] result = new double[2]; 418 double total = 0.0; 419 if (this.renderAsPercentages) { 420 total = DataUtilities.calculateColumnTotal(dataset, index, 421 validRows); 422 } 423 for (int i = 0; i < series; i++) { 424 if (isSeriesVisible(i)) { 425 double v = 0.0; 426 Number n = dataset.getValue(i, index); 427 if (n != null) { 428 v = n.doubleValue(); 429 if (this.renderAsPercentages) { 430 v = v / total; 431 } 432 } 433 if (!Double.isNaN(v)) { 434 if (v >= 0.0) { 435 result[1] += v; 436 } 437 else { 438 result[0] += v; 439 } 440 } 441 } 442 } 443 return result; 444 } 445 446 /** 447 * Returns a pair of "stack" values calculated as the mean of the two 448 * specified stack value pairs. 449 * 450 * @param stack1 the first stack pair. 451 * @param stack2 the second stack pair. 452 * 453 * @return A pair of average stack values. 454 */ 455 private double[] averageStackValues(double[] stack1, double[] stack2) { 456 double[] result = new double[2]; 457 result[0] = (stack1[0] + stack2[0]) / 2.0; 458 result[1] = (stack1[1] + stack2[1]) / 2.0; 459 return result; 460 } 461 462 /** 463 * Calculates adjusted stack values from the supplied values. The value is 464 * the mean of the supplied values, unless either of the supplied values 465 * is zero, in which case the adjusted value is zero also. 466 * 467 * @param stack1 the first stack pair. 468 * @param stack2 the second stack pair. 469 * 470 * @return A pair of average stack values. 471 */ 472 private double[] adjustedStackValues(double[] stack1, double[] stack2) { 473 double[] result = new double[2]; 474 if (stack1[0] == 0.0 || stack2[0] == 0.0) { 475 result[0] = 0.0; 476 } 477 else { 478 result[0] = (stack1[0] + stack2[0]) / 2.0; 479 } 480 if (stack1[1] == 0.0 || stack2[1] == 0.0) { 481 result[1] = 0.0; 482 } 483 else { 484 result[1] = (stack1[1] + stack2[1]) / 2.0; 485 } 486 return result; 487 } 488 489 /** 490 * Checks this instance for equality with an arbitrary object. 491 * 492 * @param obj the object (<code>null</code> not permitted). 493 * 494 * @return A boolean. 495 */ 496 @Override 497 public boolean equals(Object obj) { 498 if (obj == this) { 499 return true; 500 } 501 if (!(obj instanceof StackedAreaRenderer)) { 502 return false; 503 } 504 StackedAreaRenderer that = (StackedAreaRenderer) obj; 505 if (this.renderAsPercentages != that.renderAsPercentages) { 506 return false; 507 } 508 return super.equals(obj); 509 } 510 511 /** 512 * Calculates the stacked value of the all series up to, but not including 513 * <code>series</code> for the specified category, <code>category</code>. 514 * It returns 0.0 if <code>series</code> is the first series, i.e. 0. 515 * 516 * @param dataset the dataset (<code>null</code> not permitted). 517 * @param series the series. 518 * @param category the category. 519 * 520 * @return double returns a cumulative value for all series' values up to 521 * but excluding <code>series</code> for Object 522 * <code>category</code>. 523 * 524 * @deprecated As of 1.0.13, as the method is never used internally. 525 */ 526 protected double getPreviousHeight(CategoryDataset dataset, 527 int series, int category) { 528 529 double result = 0.0; 530 Number n; 531 double total = 0.0; 532 if (this.renderAsPercentages) { 533 total = DataUtilities.calculateColumnTotal(dataset, category); 534 } 535 for (int i = 0; i < series; i++) { 536 n = dataset.getValue(i, category); 537 if (n != null) { 538 double v = n.doubleValue(); 539 if (this.renderAsPercentages) { 540 v = v / total; 541 } 542 result += v; 543 } 544 } 545 return result; 546 547 } 548 549}