001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2009, 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-2009, 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 * Changes:
040 * --------
041 * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
042 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and
043 * CategoryToolTipGenerator interface (DG);
044 * 01-Nov-2002 : Added tooltips (DG);
045 * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis
046 * for category spacing. Renamed StackedAreaCategoryItemRenderer
047 * --> StackedAreaRenderer (DG);
048 * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
049 * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
050 * 17-Jan-2003 : Moved plot classes to a separate package (DG);
051 * 25-Mar-2003 : Implemented Serializable (DG);
052 * 13-May-2003 : Modified to take into account the plot orientation (DG);
053 * 30-Jul-2003 : Modified entity constructor (CZ);
054 * 07-Oct-2003 : Added renderer state (DG);
055 * 29-Apr-2004 : Added getRangeExtent() override (DG);
056 * 05-Nov-2004 : Modified drawItem() signature (DG);
057 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
058 * ------------- JFREECHART 1.0.x ---------------------------------------------
059 * 11-Oct-2006 : Added support for rendering data values as percentages,
060 * and added a second pass for drawing item labels (DG);
061 * 04-Feb-2009 : Fixed support for hidden series, and bug in findRangeBounds()
062 * method for null dataset (PK/DG);
063 * 04-Feb-2009 : Added item label support, and generate entities only in first
064 * pass (DG);
065 * 04-Feb-2009 : Fixed bug for renderAsPercentages == true (DG);
066 *
067 */
068
069 package org.jfree.chart.renderer.category;
070
071 import java.awt.Graphics2D;
072 import java.awt.Paint;
073 import java.awt.Shape;
074 import java.awt.geom.GeneralPath;
075 import java.awt.geom.Rectangle2D;
076 import java.io.Serializable;
077
078 import org.jfree.chart.axis.CategoryAxis;
079 import org.jfree.chart.axis.ValueAxis;
080 import org.jfree.chart.entity.EntityCollection;
081 import org.jfree.chart.event.RendererChangeEvent;
082 import org.jfree.chart.plot.CategoryPlot;
083 import org.jfree.data.DataUtilities;
084 import org.jfree.data.Range;
085 import org.jfree.data.category.CategoryDataset;
086 import org.jfree.data.general.DatasetUtilities;
087 import org.jfree.ui.RectangleEdge;
088 import org.jfree.util.PublicCloneable;
089
090 /**
091 * A renderer that draws stacked area charts for a {@link CategoryPlot}.
092 * The example shown here is generated by the
093 * <code>StackedAreaChartDemo1.java</code> program included in the
094 * JFreeChart Demo Collection:
095 * <br><br>
096 * <img src="../../../../../images/StackedAreaRendererSample.png"
097 * alt="StackedAreaRendererSample.png" />
098 */
099 public class StackedAreaRenderer extends AreaRenderer
100 implements Cloneable, PublicCloneable, Serializable {
101
102 /** For serialization. */
103 private static final long serialVersionUID = -3595635038460823663L;
104
105 /** A flag that controls whether the areas display values or percentages. */
106 private boolean renderAsPercentages;
107
108 /**
109 * Creates a new renderer.
110 */
111 public StackedAreaRenderer() {
112 this(false);
113 }
114
115 /**
116 * Creates a new renderer.
117 *
118 * @param renderAsPercentages a flag that controls whether the data values
119 * are rendered as percentages.
120 */
121 public StackedAreaRenderer(boolean renderAsPercentages) {
122 super();
123 this.renderAsPercentages = renderAsPercentages;
124 }
125
126 /**
127 * Returns <code>true</code> if the renderer displays each item value as
128 * a percentage (so that the stacked areas add to 100%), and
129 * <code>false</code> otherwise.
130 *
131 * @return A boolean.
132 *
133 * @since 1.0.3
134 */
135 public boolean getRenderAsPercentages() {
136 return this.renderAsPercentages;
137 }
138
139 /**
140 * Sets the flag that controls whether the renderer displays each item
141 * value as a percentage (so that the stacked areas add to 100%), and sends
142 * a {@link RendererChangeEvent} to all registered listeners.
143 *
144 * @param asPercentages the flag.
145 *
146 * @since 1.0.3
147 */
148 public void setRenderAsPercentages(boolean asPercentages) {
149 this.renderAsPercentages = asPercentages;
150 fireChangeEvent();
151 }
152
153 /**
154 * Returns the number of passes (<code>2</code>) required by this renderer.
155 * The first pass is used to draw the areas, the second pass is used to
156 * draw the item labels (if visible).
157 *
158 * @return The number of passes required by the renderer.
159 */
160 public int getPassCount() {
161 return 2;
162 }
163
164 /**
165 * Returns the range of values the renderer requires to display all the
166 * items from the specified dataset.
167 *
168 * @param dataset the dataset (<code>null</code> not permitted).
169 *
170 * @return The range (or <code>null</code> if the dataset is empty).
171 */
172 public Range findRangeBounds(CategoryDataset dataset) {
173 if (dataset == null) {
174 return null;
175 }
176 if (this.renderAsPercentages) {
177 return new Range(0.0, 1.0);
178 }
179 else {
180 return DatasetUtilities.findStackedRangeBounds(dataset);
181 }
182 }
183
184 /**
185 * Draw a single data item.
186 *
187 * @param g2 the graphics device.
188 * @param state the renderer state.
189 * @param dataArea the data plot area.
190 * @param plot the plot.
191 * @param domainAxis the domain axis.
192 * @param rangeAxis the range axis.
193 * @param dataset the data.
194 * @param row the row index (zero-based).
195 * @param column the column index (zero-based).
196 * @param pass the pass index.
197 */
198 public void drawItem(Graphics2D g2,
199 CategoryItemRendererState state,
200 Rectangle2D dataArea,
201 CategoryPlot plot,
202 CategoryAxis domainAxis,
203 ValueAxis rangeAxis,
204 CategoryDataset dataset,
205 int row,
206 int column,
207 int pass) {
208
209 if (!isSeriesVisible(row)) {
210 return;
211 }
212
213 // setup for collecting optional entity info...
214 Shape entityArea = null;
215 EntityCollection entities = state.getEntityCollection();
216
217 double y1 = 0.0;
218 Number n = dataset.getValue(row, column);
219 if (n != null) {
220 y1 = n.doubleValue();
221 if (this.renderAsPercentages) {
222 double total = DataUtilities.calculateColumnTotal(dataset,
223 column, state.getVisibleSeriesArray());
224 y1 = y1 / total;
225 }
226 }
227 double[] stack1 = getStackValues(dataset, row, column,
228 state.getVisibleSeriesArray());
229
230
231 // leave the y values (y1, y0) untranslated as it is going to be be
232 // stacked up later by previous series values, after this it will be
233 // translated.
234 double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
235 dataArea, plot.getDomainAxisEdge());
236
237
238 // get the previous point and the next point so we can calculate a
239 // "hot spot" for the area (used by the chart entity)...
240 double y0 = 0.0;
241 n = dataset.getValue(row, Math.max(column - 1, 0));
242 if (n != null) {
243 y0 = n.doubleValue();
244 if (this.renderAsPercentages) {
245 double total = DataUtilities.calculateColumnTotal(dataset,
246 Math.max(column - 1, 0), state.getVisibleSeriesArray());
247 y0 = y0 / total;
248 }
249 }
250 double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0),
251 state.getVisibleSeriesArray());
252
253 // FIXME: calculate xx0
254 double xx0 = domainAxis.getCategoryStart(column, getColumnCount(),
255 dataArea, plot.getDomainAxisEdge());
256
257 int itemCount = dataset.getColumnCount();
258 double y2 = 0.0;
259 n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
260 if (n != null) {
261 y2 = n.doubleValue();
262 if (this.renderAsPercentages) {
263 double total = DataUtilities.calculateColumnTotal(dataset,
264 Math.min(column + 1, itemCount - 1),
265 state.getVisibleSeriesArray());
266 y2 = y2 / total;
267 }
268 }
269 double[] stack2 = getStackValues(dataset, row, Math.min(column + 1,
270 itemCount - 1), state.getVisibleSeriesArray());
271
272 double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(),
273 dataArea, plot.getDomainAxisEdge());
274
275 // FIXME: calculate xxLeft and xxRight
276 double xxLeft = xx0;
277 double xxRight = xx2;
278
279 double[] stackLeft = averageStackValues(stack0, stack1);
280 double[] stackRight = averageStackValues(stack1, stack2);
281 double[] adjStackLeft = adjustedStackValues(stack0, stack1);
282 double[] adjStackRight = adjustedStackValues(stack1, stack2);
283
284 float transY1;
285
286 RectangleEdge edge1 = plot.getRangeAxisEdge();
287
288 GeneralPath left = new GeneralPath();
289 GeneralPath right = new GeneralPath();
290 if (y1 >= 0.0) { // handle positive value
291 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
292 edge1);
293 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
294 dataArea, edge1);
295 float transStackLeft = (float) rangeAxis.valueToJava2D(
296 adjStackLeft[1], dataArea, edge1);
297
298 // LEFT POLYGON
299 if (y0 >= 0.0) {
300 double yleft = (y0 + y1) / 2.0 + stackLeft[1];
301 float transYLeft
302 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
303 left.moveTo((float) xx1, transY1);
304 left.lineTo((float) xx1, transStack1);
305 left.lineTo((float) xxLeft, transStackLeft);
306 left.lineTo((float) xxLeft, transYLeft);
307 left.closePath();
308 }
309 else {
310 left.moveTo((float) xx1, transStack1);
311 left.lineTo((float) xx1, transY1);
312 left.lineTo((float) xxLeft, transStackLeft);
313 left.closePath();
314 }
315
316 float transStackRight = (float) rangeAxis.valueToJava2D(
317 adjStackRight[1], dataArea, edge1);
318 // RIGHT POLYGON
319 if (y2 >= 0.0) {
320 double yright = (y1 + y2) / 2.0 + stackRight[1];
321 float transYRight
322 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
323 right.moveTo((float) xx1, transStack1);
324 right.lineTo((float) xx1, transY1);
325 right.lineTo((float) xxRight, transYRight);
326 right.lineTo((float) xxRight, transStackRight);
327 right.closePath();
328 }
329 else {
330 right.moveTo((float) xx1, transStack1);
331 right.lineTo((float) xx1, transY1);
332 right.lineTo((float) xxRight, transStackRight);
333 right.closePath();
334 }
335 }
336 else { // handle negative value
337 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
338 edge1);
339 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
340 dataArea, edge1);
341 float transStackLeft = (float) rangeAxis.valueToJava2D(
342 adjStackLeft[0], dataArea, edge1);
343
344 // LEFT POLYGON
345 if (y0 >= 0.0) {
346 left.moveTo((float) xx1, transStack1);
347 left.lineTo((float) xx1, transY1);
348 left.lineTo((float) xxLeft, transStackLeft);
349 left.clone();
350 }
351 else {
352 double yleft = (y0 + y1) / 2.0 + stackLeft[0];
353 float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
354 dataArea, edge1);
355 left.moveTo((float) xx1, transY1);
356 left.lineTo((float) xx1, transStack1);
357 left.lineTo((float) xxLeft, transStackLeft);
358 left.lineTo((float) xxLeft, transYLeft);
359 left.closePath();
360 }
361 float transStackRight = (float) rangeAxis.valueToJava2D(
362 adjStackRight[0], dataArea, edge1);
363
364 // RIGHT POLYGON
365 if (y2 >= 0.0) {
366 right.moveTo((float) xx1, transStack1);
367 right.lineTo((float) xx1, transY1);
368 right.lineTo((float) xxRight, transStackRight);
369 right.closePath();
370 }
371 else {
372 double yright = (y1 + y2) / 2.0 + stackRight[0];
373 float transYRight = (float) rangeAxis.valueToJava2D(yright,
374 dataArea, edge1);
375 right.moveTo((float) xx1, transStack1);
376 right.lineTo((float) xx1, transY1);
377 right.lineTo((float) xxRight, transYRight);
378 right.lineTo((float) xxRight, transStackRight);
379 right.closePath();
380 }
381 }
382
383 if (pass == 0) {
384 Paint itemPaint = getItemPaint(row, column);
385 g2.setPaint(itemPaint);
386 g2.fill(left);
387 g2.fill(right);
388
389 // add an entity for the item...
390 if (entities != null) {
391 GeneralPath gp = new GeneralPath(left);
392 gp.append(right, false);
393 entityArea = gp;
394 addItemEntity(entities, dataset, row, column, entityArea);
395 }
396 }
397 else if (pass == 1) {
398 drawItemLabel(g2, plot.getOrientation(), dataset, row, column,
399 xx1, transY1, y1 < 0.0);
400 }
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, int[] validRows) {
419 double[] result = new double[2];
420 double total = 0.0;
421 if (this.renderAsPercentages) {
422 total = DataUtilities.calculateColumnTotal(dataset, index,
423 validRows);
424 }
425 for (int i = 0; i < series; i++) {
426 if (isSeriesVisible(i)) {
427 double v = 0.0;
428 Number n = dataset.getValue(i, index);
429 if (n != null) {
430 v = n.doubleValue();
431 if (this.renderAsPercentages) {
432 v = v / total;
433 }
434 }
435 if (!Double.isNaN(v)) {
436 if (v >= 0.0) {
437 result[1] += v;
438 }
439 else {
440 result[0] += v;
441 }
442 }
443 }
444 }
445 return result;
446 }
447
448 /**
449 * Returns a pair of "stack" values calculated as the mean of the two
450 * specified stack value pairs.
451 *
452 * @param stack1 the first stack pair.
453 * @param stack2 the second stack pair.
454 *
455 * @return A pair of average stack values.
456 */
457 private double[] averageStackValues(double[] stack1, double[] stack2) {
458 double[] result = new double[2];
459 result[0] = (stack1[0] + stack2[0]) / 2.0;
460 result[1] = (stack1[1] + stack2[1]) / 2.0;
461 return result;
462 }
463
464 /**
465 * Calculates adjusted stack values from the supplied values. The value is
466 * the mean of the supplied values, unless either of the supplied values
467 * is zero, in which case the adjusted value is zero also.
468 *
469 * @param stack1 the first stack pair.
470 * @param stack2 the second stack pair.
471 *
472 * @return A pair of average stack values.
473 */
474 private double[] adjustedStackValues(double[] stack1, double[] stack2) {
475 double[] result = new double[2];
476 if (stack1[0] == 0.0 || stack2[0] == 0.0) {
477 result[0] = 0.0;
478 }
479 else {
480 result[0] = (stack1[0] + stack2[0]) / 2.0;
481 }
482 if (stack1[1] == 0.0 || stack2[1] == 0.0) {
483 result[1] = 0.0;
484 }
485 else {
486 result[1] = (stack1[1] + stack2[1]) / 2.0;
487 }
488 return result;
489 }
490
491 /**
492 * Checks this instance for equality with an arbitrary object.
493 *
494 * @param obj the object (<code>null</code> not permitted).
495 *
496 * @return A boolean.
497 */
498 public boolean equals(Object obj) {
499 if (obj == this) {
500 return true;
501 }
502 if (!(obj instanceof StackedAreaRenderer)) {
503 return false;
504 }
505 StackedAreaRenderer that = (StackedAreaRenderer) obj;
506 if (this.renderAsPercentages != that.renderAsPercentages) {
507 return false;
508 }
509 return super.equals(obj);
510 }
511
512 /**
513 * Calculates the stacked value of the all series up to, but not including
514 * <code>series</code> for the specified category, <code>category</code>.
515 * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
516 *
517 * @param dataset the dataset (<code>null</code> not permitted).
518 * @param series the series.
519 * @param category the category.
520 *
521 * @return double returns a cumulative value for all series' values up to
522 * but excluding <code>series</code> for Object
523 * <code>category</code>.
524 *
525 * @deprecated As of 1.0.13, as the method is never used internally.
526 */
527 protected double getPreviousHeight(CategoryDataset dataset,
528 int series, int category) {
529
530 double result = 0.0;
531 Number n;
532 double total = 0.0;
533 if (this.renderAsPercentages) {
534 total = DataUtilities.calculateColumnTotal(dataset, category);
535 }
536 for (int i = 0; i < series; i++) {
537 n = dataset.getValue(i, category);
538 if (n != null) {
539 double v = n.doubleValue();
540 if (this.renderAsPercentages) {
541 v = v / total;
542 }
543 result += v;
544 }
545 }
546 return result;
547
548 }
549
550 }