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