001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, 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; 036 * Christian W. Zuckschwerdt; 037 * Peter Kolb (patch 2511330); 038 */ 039 040package org.jfree.chart.renderer.category; 041 042import java.awt.Graphics2D; 043import java.awt.Paint; 044import java.awt.Shape; 045import java.awt.geom.GeneralPath; 046import java.awt.geom.Rectangle2D; 047import java.io.Serializable; 048 049import org.jfree.chart.axis.CategoryAxis; 050import org.jfree.chart.axis.ValueAxis; 051import org.jfree.chart.entity.EntityCollection; 052import org.jfree.chart.event.RendererChangeEvent; 053import org.jfree.chart.plot.CategoryPlot; 054import org.jfree.chart.ui.RectangleEdge; 055import org.jfree.chart.util.PublicCloneable; 056import org.jfree.data.DataUtils; 057import org.jfree.data.Range; 058import org.jfree.data.category.CategoryDataset; 059import org.jfree.data.general.DatasetUtils; 060 061/** 062 * A renderer that draws stacked area charts for a {@link CategoryPlot}. 063 * The example shown here is generated by the 064 * {@code StackedAreaChartDemo1.java} program included in the 065 * JFreeChart Demo Collection: 066 * <br><br> 067 * <img src="doc-files/StackedAreaRendererSample.png" 068 * alt="StackedAreaRendererSample.png"> 069 */ 070public class StackedAreaRenderer extends AreaRenderer 071 implements Cloneable, PublicCloneable, Serializable { 072 073 /** For serialization. */ 074 private static final long serialVersionUID = -3595635038460823663L; 075 076 /** A flag that controls whether the areas display values or percentages. */ 077 private boolean renderAsPercentages; 078 079 /** 080 * Creates a new renderer. 081 */ 082 public StackedAreaRenderer() { 083 this(false); 084 } 085 086 /** 087 * Creates a new renderer. 088 * 089 * @param renderAsPercentages a flag that controls whether the data values 090 * are rendered as percentages. 091 */ 092 public StackedAreaRenderer(boolean renderAsPercentages) { 093 super(); 094 this.renderAsPercentages = renderAsPercentages; 095 } 096 097 /** 098 * Returns {@code true} if the renderer displays each item value as 099 * a percentage (so that the stacked areas add to 100%), and 100 * {@code false} otherwise. 101 * 102 * @return A boolean. 103 */ 104 public boolean getRenderAsPercentages() { 105 return this.renderAsPercentages; 106 } 107 108 /** 109 * Sets the flag that controls whether the renderer displays each item 110 * value as a percentage (so that the stacked areas add to 100%), and sends 111 * a {@link RendererChangeEvent} to all registered listeners. 112 * 113 * @param asPercentages the flag. 114 */ 115 public void setRenderAsPercentages(boolean asPercentages) { 116 this.renderAsPercentages = asPercentages; 117 fireChangeEvent(); 118 } 119 120 /** 121 * Returns the number of passes ({@code 2}) required by this renderer. 122 * The first pass is used to draw the areas, the second pass is used to 123 * draw the item labels (if visible). 124 * 125 * @return The number of passes required by the renderer. 126 */ 127 @Override 128 public int getPassCount() { 129 return 2; 130 } 131 132 /** 133 * Returns the range of values the renderer requires to display all the 134 * items from the specified dataset. 135 * 136 * @param dataset the dataset ({@code null} not permitted). 137 * 138 * @return The range (or {@code null} if the dataset is empty). 139 */ 140 @Override 141 public Range findRangeBounds(CategoryDataset dataset) { 142 if (dataset == null) { 143 return null; 144 } 145 if (this.renderAsPercentages) { 146 return new Range(0.0, 1.0); 147 } 148 else { 149 return DatasetUtils.findStackedRangeBounds(dataset); 150 } 151 } 152 153 /** 154 * Draw a single data item. 155 * 156 * @param g2 the graphics device. 157 * @param state the renderer state. 158 * @param dataArea the data plot area. 159 * @param plot the plot. 160 * @param domainAxis the domain axis. 161 * @param rangeAxis the range axis. 162 * @param dataset the data. 163 * @param row the row index (zero-based). 164 * @param column the column index (zero-based). 165 * @param pass the pass index. 166 */ 167 @Override 168 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 169 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 170 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 171 int pass) { 172 173 if (!isSeriesVisible(row)) { 174 return; 175 } 176 177 // setup for collecting optional entity info... 178 Shape entityArea; 179 EntityCollection entities = state.getEntityCollection(); 180 181 double y1 = 0.0; 182 Number n = dataset.getValue(row, column); 183 if (n != null) { 184 y1 = n.doubleValue(); 185 if (this.renderAsPercentages) { 186 double total = DataUtils.calculateColumnTotal(dataset, 187 column, state.getVisibleSeriesArray()); 188 y1 = y1 / total; 189 } 190 } 191 double[] stack1 = getStackValues(dataset, row, column, 192 state.getVisibleSeriesArray()); 193 194 195 // leave the y values (y1, y0) untranslated as it is going to be be 196 // stacked up later by previous series values, after this it will be 197 // translated. 198 double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 199 dataArea, plot.getDomainAxisEdge()); 200 201 202 // get the previous point and the next point so we can calculate a 203 // "hot spot" for the area (used by the chart entity)... 204 double y0 = 0.0; 205 n = dataset.getValue(row, Math.max(column - 1, 0)); 206 if (n != null) { 207 y0 = n.doubleValue(); 208 if (this.renderAsPercentages) { 209 double total = DataUtils.calculateColumnTotal(dataset, 210 Math.max(column - 1, 0), state.getVisibleSeriesArray()); 211 y0 = y0 / total; 212 } 213 } 214 double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0), 215 state.getVisibleSeriesArray()); 216 217 // FIXME: calculate xx0 218 double xx0 = domainAxis.getCategoryStart(column, getColumnCount(), 219 dataArea, plot.getDomainAxisEdge()); 220 221 int itemCount = dataset.getColumnCount(); 222 double y2 = 0.0; 223 n = dataset.getValue(row, Math.min(column + 1, itemCount - 1)); 224 if (n != null) { 225 y2 = n.doubleValue(); 226 if (this.renderAsPercentages) { 227 double total = DataUtils.calculateColumnTotal(dataset, 228 Math.min(column + 1, itemCount - 1), 229 state.getVisibleSeriesArray()); 230 y2 = y2 / total; 231 } 232 } 233 double[] stack2 = getStackValues(dataset, row, Math.min(column + 1, 234 itemCount - 1), state.getVisibleSeriesArray()); 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 if (pass == 0) { 348 Paint itemPaint = getItemPaint(row, column); 349 g2.setPaint(itemPaint); 350 g2.fill(left); 351 g2.fill(right); 352 353 // add an entity for the item... 354 if (entities != null) { 355 GeneralPath gp = new GeneralPath(left); 356 gp.append(right, false); 357 entityArea = gp; 358 addItemEntity(entities, dataset, row, column, entityArea); 359 } 360 } 361 else if (pass == 1) { 362 drawItemLabel(g2, plot.getOrientation(), dataset, row, column, 363 xx1, transY1, y1 < 0.0); 364 } 365 366 } 367 368 /** 369 * Calculates the stacked values (one positive and one negative) of all 370 * series up to, but not including, {@code series} for the specified 371 * item. It returns [0.0, 0.0] if {@code series} is the first series. 372 * 373 * @param dataset the dataset ({@code null} not permitted). 374 * @param series the series index. 375 * @param index the item index. 376 * @param validRows the valid rows. 377 * 378 * @return An array containing the cumulative negative and positive values 379 * for all series values up to but excluding {@code series} 380 * for {@code index}. 381 */ 382 protected double[] getStackValues(CategoryDataset dataset, 383 int series, int index, int[] validRows) { 384 double[] result = new double[2]; 385 double total = 0.0; 386 if (this.renderAsPercentages) { 387 total = DataUtils.calculateColumnTotal(dataset, index, 388 validRows); 389 } 390 for (int i = 0; i < series; i++) { 391 if (isSeriesVisible(i)) { 392 double v = 0.0; 393 Number n = dataset.getValue(i, index); 394 if (n != null) { 395 v = n.doubleValue(); 396 if (this.renderAsPercentages) { 397 v = v / total; 398 } 399 } 400 if (!Double.isNaN(v)) { 401 if (v >= 0.0) { 402 result[1] += v; 403 } 404 else { 405 result[0] += v; 406 } 407 } 408 } 409 } 410 return result; 411 } 412 413 /** 414 * Returns a pair of "stack" values calculated as the mean of the two 415 * specified stack value pairs. 416 * 417 * @param stack1 the first stack pair. 418 * @param stack2 the second stack pair. 419 * 420 * @return A pair of average stack values. 421 */ 422 private double[] averageStackValues(double[] stack1, double[] stack2) { 423 double[] result = new double[2]; 424 result[0] = (stack1[0] + stack2[0]) / 2.0; 425 result[1] = (stack1[1] + stack2[1]) / 2.0; 426 return result; 427 } 428 429 /** 430 * Calculates adjusted stack values from the supplied values. The value is 431 * the mean of the supplied values, unless either of the supplied values 432 * is zero, in which case the adjusted value is zero also. 433 * 434 * @param stack1 the first stack pair. 435 * @param stack2 the second stack pair. 436 * 437 * @return A pair of average stack values. 438 */ 439 private double[] adjustedStackValues(double[] stack1, double[] stack2) { 440 double[] result = new double[2]; 441 if (stack1[0] == 0.0 || stack2[0] == 0.0) { 442 result[0] = 0.0; 443 } 444 else { 445 result[0] = (stack1[0] + stack2[0]) / 2.0; 446 } 447 if (stack1[1] == 0.0 || stack2[1] == 0.0) { 448 result[1] = 0.0; 449 } 450 else { 451 result[1] = (stack1[1] + stack2[1]) / 2.0; 452 } 453 return result; 454 } 455 456 /** 457 * Checks this instance for equality with an arbitrary object. 458 * 459 * @param obj the object ({@code null} not permitted). 460 * 461 * @return A boolean. 462 */ 463 @Override 464 public boolean equals(Object obj) { 465 if (obj == this) { 466 return true; 467 } 468 if (!(obj instanceof StackedAreaRenderer)) { 469 return false; 470 } 471 StackedAreaRenderer that = (StackedAreaRenderer) obj; 472 if (this.renderAsPercentages != that.renderAsPercentages) { 473 return false; 474 } 475 return super.equals(obj); 476 } 477 478}