001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2005, 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-2006, 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 * $Id: StackedAreaRenderer.java,v 1.6.2.3 2006/10/11 16:25:50 mungady Exp $ 039 * 040 * Changes: 041 * -------- 042 * 20-Sep-2002 : Version 1, contributed by Dan Rivett; 043 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 044 * CategoryToolTipGenerator interface (DG); 045 * 01-Nov-2002 : Added tooltips (DG); 046 * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis 047 * for category spacing. Renamed StackedAreaCategoryItemRenderer 048 * --> StackedAreaRenderer (DG); 049 * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG); 050 * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG); 051 * 17-Jan-2003 : Moved plot classes to a separate package (DG); 052 * 25-Mar-2003 : Implemented Serializable (DG); 053 * 13-May-2003 : Modified to take into account the plot orientation (DG); 054 * 30-Jul-2003 : Modified entity constructor (CZ); 055 * 07-Oct-2003 : Added renderer state (DG); 056 * 29-Apr-2004 : Added getRangeExtent() override (DG); 057 * 05-Nov-2004 : Modified drawItem() signature (DG); 058 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG); 059 * ------------- JFREECHART 1.0.x --------------------------------------------- 060 * 11-Oct-2006 : Added support for rendering data values as percentages, 061 * and added a second pass for drawing item labels (DG); 062 * 063 */ 064 065 package org.jfree.chart.renderer.category; 066 067 import java.awt.Graphics2D; 068 import java.awt.Polygon; 069 import java.awt.Shape; 070 import java.awt.geom.Rectangle2D; 071 import java.io.Serializable; 072 073 import org.jfree.chart.axis.CategoryAxis; 074 import org.jfree.chart.axis.ValueAxis; 075 import org.jfree.chart.entity.EntityCollection; 076 import org.jfree.chart.event.RendererChangeEvent; 077 import org.jfree.chart.plot.CategoryPlot; 078 import org.jfree.chart.plot.PlotOrientation; 079 import org.jfree.data.DataUtilities; 080 import org.jfree.data.Range; 081 import org.jfree.data.category.CategoryDataset; 082 import org.jfree.data.general.DatasetUtilities; 083 import org.jfree.ui.RectangleEdge; 084 import org.jfree.util.PublicCloneable; 085 086 /** 087 * A renderer that draws stacked area charts for a 088 * {@link org.jfree.chart.plot.CategoryPlot}. 089 */ 090 public class StackedAreaRenderer extends AreaRenderer 091 implements Cloneable, PublicCloneable, 092 Serializable { 093 094 /** For serialization. */ 095 private static final long serialVersionUID = -3595635038460823663L; 096 097 /** A flag that controls whether the areas display values or percentages. */ 098 private boolean renderAsPercentages; 099 100 /** 101 * Creates a new renderer. 102 */ 103 public StackedAreaRenderer() { 104 this(false); 105 } 106 107 /** 108 * Creates a new renderer. 109 * 110 * @param renderAsPercentages a flag that controls whether the data values 111 * are rendered as percentages. 112 */ 113 public StackedAreaRenderer(boolean renderAsPercentages) { 114 super(); 115 this.renderAsPercentages = renderAsPercentages; 116 } 117 118 /** 119 * Returns <code>true</code> if the renderer displays each item value as 120 * a percentage (so that the stacked areas add to 100%), and 121 * <code>false</code> otherwise. 122 * 123 * @return A boolean. 124 * 125 * @since 1.0.3 126 */ 127 public boolean getRenderAsPercentages() { 128 return this.renderAsPercentages; 129 } 130 131 /** 132 * Sets the flag that controls whether the renderer displays each item 133 * value as a percentage (so that the stacked areas add to 100%), and sends 134 * a {@link RendererChangeEvent} to all registered listeners. 135 * 136 * @param asPercentages the flag. 137 * 138 * @since 1.0.3 139 */ 140 public void setRenderAsPercentages(boolean asPercentages) { 141 this.renderAsPercentages = asPercentages; 142 notifyListeners(new RendererChangeEvent(this)); 143 } 144 145 /** 146 * Returns the number of passes (<code>2</code>) required by this renderer. 147 * The first pass is used to draw the bars, the second pass is used to 148 * draw the item labels (if visible). 149 * 150 * @return The number of passes required by the renderer. 151 */ 152 public int getPassCount() { 153 return 2; 154 } 155 156 /** 157 * Returns the range of values the renderer requires to display all the 158 * items from the specified dataset. 159 * 160 * @param dataset the dataset (<code>null</code> not permitted). 161 * 162 * @return The range (or <code>null</code> if the dataset is empty). 163 */ 164 public Range findRangeBounds(CategoryDataset dataset) { 165 if (this.renderAsPercentages) { 166 return new Range(0.0, 1.0); 167 } 168 else { 169 return DatasetUtilities.findStackedRangeBounds(dataset); 170 } 171 } 172 173 /** 174 * Draw a single data item. 175 * 176 * @param g2 the graphics device. 177 * @param state the renderer state. 178 * @param dataArea the data plot area. 179 * @param plot the plot. 180 * @param domainAxis the domain axis. 181 * @param rangeAxis the range axis. 182 * @param dataset the data. 183 * @param row the row index (zero-based). 184 * @param column the column index (zero-based). 185 * @param pass the pass index. 186 */ 187 public void drawItem(Graphics2D g2, 188 CategoryItemRendererState state, 189 Rectangle2D dataArea, 190 CategoryPlot plot, 191 CategoryAxis domainAxis, 192 ValueAxis rangeAxis, 193 CategoryDataset dataset, 194 int row, 195 int column, 196 int pass) { 197 198 // plot non-null values... 199 Number dataValue = dataset.getValue(row, column); 200 if (dataValue == null) { 201 return; 202 } 203 204 double value = dataValue.doubleValue(); 205 double total = 0.0; // only needed if calculating percentages 206 if (this.renderAsPercentages) { 207 total = DataUtilities.calculateColumnTotal(dataset, column); 208 value = value / total; 209 } 210 211 // leave the y values (y1, y0) untranslated as it is going to be be 212 // stacked up later by previous series values, after this it will be 213 // translated. 214 double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 215 dataArea, plot.getDomainAxisEdge()); 216 217 double previousHeightx1 = getPreviousHeight(dataset, row, column); 218 double y1 = value + previousHeightx1; 219 RectangleEdge location = plot.getRangeAxisEdge(); 220 double yy1 = rangeAxis.valueToJava2D(y1, dataArea, location); 221 222 g2.setPaint(getItemPaint(row, column)); 223 g2.setStroke(getItemStroke(row, column)); 224 225 // in column zero, the only job to do is draw any visible item labels 226 // and this is done in the second pass... 227 if (column == 0) { 228 if (pass == 1) { 229 // draw item labels, if visible 230 if (isItemLabelVisible(row, column)) { 231 drawItemLabel(g2, plot.getOrientation(), dataset, row, column, 232 xx1, yy1, (y1 < 0.0)); 233 } 234 } 235 } 236 else { 237 Number previousValue = dataset.getValue(row, column - 1); 238 if (previousValue != null) { 239 240 double xx0 = domainAxis.getCategoryMiddle(column - 1, 241 getColumnCount(), dataArea, plot.getDomainAxisEdge()); 242 double y0 = previousValue.doubleValue(); 243 if (this.renderAsPercentages) { 244 total = DataUtilities.calculateColumnTotal(dataset, 245 column - 1); 246 y0 = y0 / total; 247 } 248 249 250 // Get the previous height, but this will be different for both 251 // y0 and y1 as the previous series values could differ. 252 double previousHeightx0 = getPreviousHeight(dataset, row, 253 column - 1); 254 255 // Now stack the current y values on top of the previous values. 256 y0 += previousHeightx0; 257 258 // Now translate the previous heights 259 double previousHeightxx0 = rangeAxis.valueToJava2D( 260 previousHeightx0, dataArea, location); 261 double previousHeightxx1 = rangeAxis.valueToJava2D( 262 previousHeightx1, dataArea, location); 263 264 // Now translate the current y values. 265 double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location); 266 267 if (pass == 0) { 268 Polygon p = null; 269 PlotOrientation orientation = plot.getOrientation(); 270 if (orientation == PlotOrientation.HORIZONTAL) { 271 p = new Polygon(); 272 p.addPoint((int) yy0, (int) xx0); 273 p.addPoint((int) yy1, (int) xx1); 274 p.addPoint((int) previousHeightxx1, (int) xx1); 275 p.addPoint((int) previousHeightxx0, (int) xx0); 276 } 277 else if (orientation == PlotOrientation.VERTICAL) { 278 p = new Polygon(); 279 p.addPoint((int) xx0, (int) yy0); 280 p.addPoint((int) xx1, (int) yy1); 281 p.addPoint((int) xx1, (int) previousHeightxx1); 282 p.addPoint((int) xx0, (int) previousHeightxx0); 283 } 284 g2.setPaint(getItemPaint(row, column)); 285 g2.setStroke(getItemStroke(row, column)); 286 g2.fill(p); 287 } 288 else { 289 if (isItemLabelVisible(row, column)) { 290 drawItemLabel(g2, plot.getOrientation(), dataset, row, 291 column, xx1, yy1, (y1 < 0.0)); 292 } 293 } 294 } 295 296 297 } 298 299 300 // add an item entity, if this information is being collected 301 EntityCollection entities = state.getEntityCollection(); 302 if (entities != null) { 303 Shape shape = new Rectangle2D.Double(xx1 - 3.0, yy1 - 3.0, 6.0, 6.0); 304 addItemEntity(entities, dataset, row, column, shape); 305 } 306 307 } 308 309 /** 310 * Calculates the stacked value of the all series up to, but not including 311 * <code>series</code> for the specified category, <code>category</code>. 312 * It returns 0.0 if <code>series</code> is the first series, i.e. 0. 313 * 314 * @param dataset the dataset (<code>null</code> not permitted). 315 * @param series the series. 316 * @param category the category. 317 * 318 * @return double returns a cumulative value for all series' values up to 319 * but excluding <code>series</code> for Object 320 * <code>category</code>. 321 */ 322 protected double getPreviousHeight(CategoryDataset dataset, 323 int series, int category) { 324 325 double result = 0.0; 326 Number n; 327 double total = 0.0; 328 if (this.renderAsPercentages) { 329 total = DataUtilities.calculateColumnTotal(dataset, category); 330 } 331 for (int i = 0; i < series; i++) { 332 n = dataset.getValue(i, category); 333 if (n != null) { 334 double v = n.doubleValue(); 335 if (this.renderAsPercentages) { 336 v = v / total; 337 } 338 result += v; 339 } 340 } 341 return result; 342 343 } 344 345 /** 346 * Checks this instance for equality with an arbitrary object. 347 * 348 * @param obj the object (<code>null</code> not permitted). 349 * 350 * @return A boolean. 351 */ 352 public boolean equals(Object obj) { 353 if (obj == this) { 354 return true; 355 } 356 if (! (obj instanceof StackedAreaRenderer)) { 357 return false; 358 } 359 StackedAreaRenderer that = (StackedAreaRenderer) obj; 360 if (this.renderAsPercentages != that.renderAsPercentages) { 361 return false; 362 } 363 return super.equals(obj); 364 } 365 }