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