001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, 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-2016, 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 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 * @since 1.0.3 105 */ 106 public boolean getRenderAsPercentages() { 107 return this.renderAsPercentages; 108 } 109 110 /** 111 * Sets the flag that controls whether the renderer displays each item 112 * value as a percentage (so that the stacked areas add to 100%), and sends 113 * a {@link RendererChangeEvent} to all registered listeners. 114 * 115 * @param asPercentages the flag. 116 * 117 * @since 1.0.3 118 */ 119 public void setRenderAsPercentages(boolean asPercentages) { 120 this.renderAsPercentages = asPercentages; 121 fireChangeEvent(); 122 } 123 124 /** 125 * Returns the number of passes ({@code 2}) required by this renderer. 126 * The first pass is used to draw the areas, the second pass is used to 127 * draw the item labels (if visible). 128 * 129 * @return The number of passes required by the renderer. 130 */ 131 @Override 132 public int getPassCount() { 133 return 2; 134 } 135 136 /** 137 * Returns the range of values the renderer requires to display all the 138 * items from the specified dataset. 139 * 140 * @param dataset the dataset ({@code null} not permitted). 141 * 142 * @return The range (or {@code null} if the dataset is empty). 143 */ 144 @Override 145 public Range findRangeBounds(CategoryDataset dataset) { 146 if (dataset == null) { 147 return null; 148 } 149 if (this.renderAsPercentages) { 150 return new Range(0.0, 1.0); 151 } 152 else { 153 return DatasetUtils.findStackedRangeBounds(dataset); 154 } 155 } 156 157 /** 158 * Draw a single data item. 159 * 160 * @param g2 the graphics device. 161 * @param state the renderer state. 162 * @param dataArea the data plot area. 163 * @param plot the plot. 164 * @param domainAxis the domain axis. 165 * @param rangeAxis the range axis. 166 * @param dataset the data. 167 * @param row the row index (zero-based). 168 * @param column the column index (zero-based). 169 * @param pass the pass index. 170 */ 171 @Override 172 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 173 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 174 ValueAxis rangeAxis, CategoryDataset dataset, int row, int column, 175 int pass) { 176 177 if (!isSeriesVisible(row)) { 178 return; 179 } 180 181 // setup for collecting optional entity info... 182 Shape entityArea; 183 EntityCollection entities = state.getEntityCollection(); 184 185 double y1 = 0.0; 186 Number n = dataset.getValue(row, column); 187 if (n != null) { 188 y1 = n.doubleValue(); 189 if (this.renderAsPercentages) { 190 double total = DataUtils.calculateColumnTotal(dataset, 191 column, state.getVisibleSeriesArray()); 192 y1 = y1 / total; 193 } 194 } 195 double[] stack1 = getStackValues(dataset, row, column, 196 state.getVisibleSeriesArray()); 197 198 199 // leave the y values (y1, y0) untranslated as it is going to be be 200 // stacked up later by previous series values, after this it will be 201 // translated. 202 double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 203 dataArea, plot.getDomainAxisEdge()); 204 205 206 // get the previous point and the next point so we can calculate a 207 // "hot spot" for the area (used by the chart entity)... 208 double y0 = 0.0; 209 n = dataset.getValue(row, Math.max(column - 1, 0)); 210 if (n != null) { 211 y0 = n.doubleValue(); 212 if (this.renderAsPercentages) { 213 double total = DataUtils.calculateColumnTotal(dataset, 214 Math.max(column - 1, 0), state.getVisibleSeriesArray()); 215 y0 = y0 / total; 216 } 217 } 218 double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0), 219 state.getVisibleSeriesArray()); 220 221 // FIXME: calculate xx0 222 double xx0 = domainAxis.getCategoryStart(column, getColumnCount(), 223 dataArea, plot.getDomainAxisEdge()); 224 225 int itemCount = dataset.getColumnCount(); 226 double y2 = 0.0; 227 n = dataset.getValue(row, Math.min(column + 1, itemCount - 1)); 228 if (n != null) { 229 y2 = n.doubleValue(); 230 if (this.renderAsPercentages) { 231 double total = DataUtils.calculateColumnTotal(dataset, 232 Math.min(column + 1, itemCount - 1), 233 state.getVisibleSeriesArray()); 234 y2 = y2 / total; 235 } 236 } 237 double[] stack2 = getStackValues(dataset, row, Math.min(column + 1, 238 itemCount - 1), state.getVisibleSeriesArray()); 239 240 double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(), 241 dataArea, plot.getDomainAxisEdge()); 242 243 // FIXME: calculate xxLeft and xxRight 244 double xxLeft = xx0; 245 double xxRight = xx2; 246 247 double[] stackLeft = averageStackValues(stack0, stack1); 248 double[] stackRight = averageStackValues(stack1, stack2); 249 double[] adjStackLeft = adjustedStackValues(stack0, stack1); 250 double[] adjStackRight = adjustedStackValues(stack1, stack2); 251 252 float transY1; 253 254 RectangleEdge edge1 = plot.getRangeAxisEdge(); 255 256 GeneralPath left = new GeneralPath(); 257 GeneralPath right = new GeneralPath(); 258 if (y1 >= 0.0) { // handle positive value 259 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, 260 edge1); 261 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], 262 dataArea, edge1); 263 float transStackLeft = (float) rangeAxis.valueToJava2D( 264 adjStackLeft[1], dataArea, edge1); 265 266 // LEFT POLYGON 267 if (y0 >= 0.0) { 268 double yleft = (y0 + y1) / 2.0 + stackLeft[1]; 269 float transYLeft 270 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1); 271 left.moveTo((float) xx1, transY1); 272 left.lineTo((float) xx1, transStack1); 273 left.lineTo((float) xxLeft, transStackLeft); 274 left.lineTo((float) xxLeft, transYLeft); 275 left.closePath(); 276 } 277 else { 278 left.moveTo((float) xx1, transStack1); 279 left.lineTo((float) xx1, transY1); 280 left.lineTo((float) xxLeft, transStackLeft); 281 left.closePath(); 282 } 283 284 float transStackRight = (float) rangeAxis.valueToJava2D( 285 adjStackRight[1], dataArea, edge1); 286 // RIGHT POLYGON 287 if (y2 >= 0.0) { 288 double yright = (y1 + y2) / 2.0 + stackRight[1]; 289 float transYRight 290 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1); 291 right.moveTo((float) xx1, transStack1); 292 right.lineTo((float) xx1, transY1); 293 right.lineTo((float) xxRight, transYRight); 294 right.lineTo((float) xxRight, transStackRight); 295 right.closePath(); 296 } 297 else { 298 right.moveTo((float) xx1, transStack1); 299 right.lineTo((float) xx1, transY1); 300 right.lineTo((float) xxRight, transStackRight); 301 right.closePath(); 302 } 303 } 304 else { // handle negative value 305 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea, 306 edge1); 307 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], 308 dataArea, edge1); 309 float transStackLeft = (float) rangeAxis.valueToJava2D( 310 adjStackLeft[0], dataArea, edge1); 311 312 // LEFT POLYGON 313 if (y0 >= 0.0) { 314 left.moveTo((float) xx1, transStack1); 315 left.lineTo((float) xx1, transY1); 316 left.lineTo((float) xxLeft, transStackLeft); 317 left.clone(); 318 } 319 else { 320 double yleft = (y0 + y1) / 2.0 + stackLeft[0]; 321 float transYLeft = (float) rangeAxis.valueToJava2D(yleft, 322 dataArea, edge1); 323 left.moveTo((float) xx1, transY1); 324 left.lineTo((float) xx1, transStack1); 325 left.lineTo((float) xxLeft, transStackLeft); 326 left.lineTo((float) xxLeft, transYLeft); 327 left.closePath(); 328 } 329 float transStackRight = (float) rangeAxis.valueToJava2D( 330 adjStackRight[0], dataArea, edge1); 331 332 // RIGHT POLYGON 333 if (y2 >= 0.0) { 334 right.moveTo((float) xx1, transStack1); 335 right.lineTo((float) xx1, transY1); 336 right.lineTo((float) xxRight, transStackRight); 337 right.closePath(); 338 } 339 else { 340 double yright = (y1 + y2) / 2.0 + stackRight[0]; 341 float transYRight = (float) rangeAxis.valueToJava2D(yright, 342 dataArea, edge1); 343 right.moveTo((float) xx1, transStack1); 344 right.lineTo((float) xx1, transY1); 345 right.lineTo((float) xxRight, transYRight); 346 right.lineTo((float) xxRight, transStackRight); 347 right.closePath(); 348 } 349 } 350 351 if (pass == 0) { 352 Paint itemPaint = getItemPaint(row, column); 353 g2.setPaint(itemPaint); 354 g2.fill(left); 355 g2.fill(right); 356 357 // add an entity for the item... 358 if (entities != null) { 359 GeneralPath gp = new GeneralPath(left); 360 gp.append(right, false); 361 entityArea = gp; 362 addItemEntity(entities, dataset, row, column, entityArea); 363 } 364 } 365 else if (pass == 1) { 366 drawItemLabel(g2, plot.getOrientation(), dataset, row, column, 367 xx1, transY1, y1 < 0.0); 368 } 369 370 } 371 372 /** 373 * Calculates the stacked values (one positive and one negative) of all 374 * series up to, but not including, {@code series} for the specified 375 * item. It returns [0.0, 0.0] if {@code series} is the first series. 376 * 377 * @param dataset the dataset ({@code null} not permitted). 378 * @param series the series index. 379 * @param index the item index. 380 * @param validRows the valid rows. 381 * 382 * @return An array containing the cumulative negative and positive values 383 * for all series values up to but excluding {@code series} 384 * for {@code index}. 385 */ 386 protected double[] getStackValues(CategoryDataset dataset, 387 int series, int index, int[] validRows) { 388 double[] result = new double[2]; 389 double total = 0.0; 390 if (this.renderAsPercentages) { 391 total = DataUtils.calculateColumnTotal(dataset, index, 392 validRows); 393 } 394 for (int i = 0; i < series; i++) { 395 if (isSeriesVisible(i)) { 396 double v = 0.0; 397 Number n = dataset.getValue(i, index); 398 if (n != null) { 399 v = n.doubleValue(); 400 if (this.renderAsPercentages) { 401 v = v / total; 402 } 403 } 404 if (!Double.isNaN(v)) { 405 if (v >= 0.0) { 406 result[1] += v; 407 } 408 else { 409 result[0] += v; 410 } 411 } 412 } 413 } 414 return result; 415 } 416 417 /** 418 * Returns a pair of "stack" values calculated as the mean of the two 419 * specified stack value pairs. 420 * 421 * @param stack1 the first stack pair. 422 * @param stack2 the second stack pair. 423 * 424 * @return A pair of average stack values. 425 */ 426 private double[] averageStackValues(double[] stack1, double[] stack2) { 427 double[] result = new double[2]; 428 result[0] = (stack1[0] + stack2[0]) / 2.0; 429 result[1] = (stack1[1] + stack2[1]) / 2.0; 430 return result; 431 } 432 433 /** 434 * Calculates adjusted stack values from the supplied values. The value is 435 * the mean of the supplied values, unless either of the supplied values 436 * is zero, in which case the adjusted value is zero also. 437 * 438 * @param stack1 the first stack pair. 439 * @param stack2 the second stack pair. 440 * 441 * @return A pair of average stack values. 442 */ 443 private double[] adjustedStackValues(double[] stack1, double[] stack2) { 444 double[] result = new double[2]; 445 if (stack1[0] == 0.0 || stack2[0] == 0.0) { 446 result[0] = 0.0; 447 } 448 else { 449 result[0] = (stack1[0] + stack2[0]) / 2.0; 450 } 451 if (stack1[1] == 0.0 || stack2[1] == 0.0) { 452 result[1] = 0.0; 453 } 454 else { 455 result[1] = (stack1[1] + stack2[1]) / 2.0; 456 } 457 return result; 458 } 459 460 /** 461 * Checks this instance for equality with an arbitrary object. 462 * 463 * @param obj the object ({@code null} not permitted). 464 * 465 * @return A boolean. 466 */ 467 @Override 468 public boolean equals(Object obj) { 469 if (obj == this) { 470 return true; 471 } 472 if (!(obj instanceof StackedAreaRenderer)) { 473 return false; 474 } 475 StackedAreaRenderer that = (StackedAreaRenderer) obj; 476 if (this.renderAsPercentages != that.renderAsPercentages) { 477 return false; 478 } 479 return super.equals(obj); 480 } 481 482}