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 }