001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2006, 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.4 2007/04/20 08:58:05 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.Paint;
069 import java.awt.Shape;
070 import java.awt.geom.GeneralPath;
071 import java.awt.geom.Rectangle2D;
072 import java.io.Serializable;
073
074 import org.jfree.chart.axis.CategoryAxis;
075 import org.jfree.chart.axis.ValueAxis;
076 import org.jfree.chart.entity.EntityCollection;
077 import org.jfree.chart.event.RendererChangeEvent;
078 import org.jfree.chart.plot.CategoryPlot;
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 // setup for collecting optional entity info...
199 Shape entityArea = null;
200 EntityCollection entities = state.getEntityCollection();
201
202 double y1 = 0.0;
203 Number n = dataset.getValue(row, column);
204 if (n != null) {
205 y1 = n.doubleValue();
206 }
207 double[] stack1 = getStackValues(dataset, row, column);
208
209
210 // leave the y values (y1, y0) untranslated as it is going to be be
211 // stacked up later by previous series values, after this it will be
212 // translated.
213 double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
214 dataArea, plot.getDomainAxisEdge());
215
216
217 // get the previous point and the next point so we can calculate a
218 // "hot spot" for the area (used by the chart entity)...
219 double y0 = 0.0;
220 n = dataset.getValue(row, Math.max(column - 1, 0));
221 if (n != null) {
222 y0 = n.doubleValue();
223 }
224 double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0));
225
226 // FIXME: calculate xx0
227 double xx0 = domainAxis.getCategoryStart(column, getColumnCount(),
228 dataArea, plot.getDomainAxisEdge());
229
230 int itemCount = dataset.getColumnCount();
231 double y2 = 0.0;
232 n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
233 if (n != null) {
234 y2 = n.doubleValue();
235 }
236 double[] stack2 = getStackValues(dataset, row, Math.min(column + 1,
237 itemCount - 1));
238
239 double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(),
240 dataArea, plot.getDomainAxisEdge());
241
242 // FIXME: calculate xxLeft and xxRight
243 double xxLeft = xx0;
244 double xxRight = xx2;
245
246 double[] stackLeft = averageStackValues(stack0, stack1);
247 double[] stackRight = averageStackValues(stack1, stack2);
248 double[] adjStackLeft = adjustedStackValues(stack0, stack1);
249 double[] adjStackRight = adjustedStackValues(stack1, stack2);
250
251 float transY1;
252
253 RectangleEdge edge1 = plot.getRangeAxisEdge();
254
255 GeneralPath left = new GeneralPath();
256 GeneralPath right = new GeneralPath();
257 if (y1 >= 0.0) { // handle positive value
258 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
259 edge1);
260 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
261 dataArea, edge1);
262 float transStackLeft = (float) rangeAxis.valueToJava2D(
263 adjStackLeft[1], dataArea, edge1);
264
265 // LEFT POLYGON
266 if (y0 >= 0.0) {
267 double yleft = (y0 + y1) / 2.0 + stackLeft[1];
268 float transYLeft
269 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
270 left.moveTo((float) xx1, transY1);
271 left.lineTo((float) xx1, transStack1);
272 left.lineTo((float) xxLeft, transStackLeft);
273 left.lineTo((float) xxLeft, transYLeft);
274 left.closePath();
275 }
276 else {
277 left.moveTo((float) xx1, transStack1);
278 left.lineTo((float) xx1, transY1);
279 left.lineTo((float) xxLeft, transStackLeft);
280 left.closePath();
281 }
282
283 float transStackRight = (float) rangeAxis.valueToJava2D(
284 adjStackRight[1], dataArea, edge1);
285 // RIGHT POLYGON
286 if (y2 >= 0.0) {
287 double yright = (y1 + y2) / 2.0 + stackRight[1];
288 float transYRight
289 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
290 right.moveTo((float) xx1, transStack1);
291 right.lineTo((float) xx1, transY1);
292 right.lineTo((float) xxRight, transYRight);
293 right.lineTo((float) xxRight, transStackRight);
294 right.closePath();
295 }
296 else {
297 right.moveTo((float) xx1, transStack1);
298 right.lineTo((float) xx1, transY1);
299 right.lineTo((float) xxRight, transStackRight);
300 right.closePath();
301 }
302 }
303 else { // handle negative value
304 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
305 edge1);
306 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
307 dataArea, edge1);
308 float transStackLeft = (float) rangeAxis.valueToJava2D(
309 adjStackLeft[0], dataArea, edge1);
310
311 // LEFT POLYGON
312 if (y0 >= 0.0) {
313 left.moveTo((float) xx1, transStack1);
314 left.lineTo((float) xx1, transY1);
315 left.lineTo((float) xxLeft, transStackLeft);
316 left.clone();
317 }
318 else {
319 double yleft = (y0 + y1) / 2.0 + stackLeft[0];
320 float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
321 dataArea, edge1);
322 left.moveTo((float) xx1, transY1);
323 left.lineTo((float) xx1, transStack1);
324 left.lineTo((float) xxLeft, transStackLeft);
325 left.lineTo((float) xxLeft, transYLeft);
326 left.closePath();
327 }
328 float transStackRight = (float) rangeAxis.valueToJava2D(
329 adjStackRight[0], dataArea, edge1);
330
331 // RIGHT POLYGON
332 if (y2 >= 0.0) {
333 right.moveTo((float) xx1, transStack1);
334 right.lineTo((float) xx1, transY1);
335 right.lineTo((float) xxRight, transStackRight);
336 right.closePath();
337 }
338 else {
339 double yright = (y1 + y2) / 2.0 + stackRight[0];
340 float transYRight = (float) rangeAxis.valueToJava2D(yright,
341 dataArea, edge1);
342 right.moveTo((float) xx1, transStack1);
343 right.lineTo((float) xx1, transY1);
344 right.lineTo((float) xxRight, transYRight);
345 right.lineTo((float) xxRight, transStackRight);
346 right.closePath();
347 }
348 }
349
350 g2.setPaint(getItemPaint(row, column));
351 g2.setStroke(getItemStroke(row, column));
352
353 // Get series Paint and Stroke
354 Paint itemPaint = getItemPaint(row, column);
355 if (pass == 0) {
356 g2.setPaint(itemPaint);
357 g2.fill(left);
358 g2.fill(right);
359 }
360
361 // add an entity for the item...
362 if (entities != null) {
363 GeneralPath gp = new GeneralPath(left);
364 gp.append(right, false);
365 entityArea = gp;
366 addItemEntity(entities, dataset, row, column, entityArea);
367 }
368
369 }
370
371 // /**
372 // * Draw a single data item.
373 // *
374 // * @param g2 the graphics device.
375 // * @param state the renderer state.
376 // * @param dataArea the data plot area.
377 // * @param plot the plot.
378 // * @param domainAxis the domain axis.
379 // * @param rangeAxis the range axis.
380 // * @param dataset the data.
381 // * @param row the row index (zero-based).
382 // * @param column the column index (zero-based).
383 // * @param pass the pass index.
384 // */
385 // public void drawItem(Graphics2D g2,
386 // CategoryItemRendererState state,
387 // Rectangle2D dataArea,
388 // CategoryPlot plot,
389 // CategoryAxis domainAxis,
390 // ValueAxis rangeAxis,
391 // CategoryDataset dataset,
392 // int row,
393 // int column,
394 // int pass) {
395 //
396 // // plot non-null values...
397 // Number dataValue = dataset.getValue(row, column);
398 // if (dataValue == null) {
399 // return;
400 // }
401 //
402 // double value = dataValue.doubleValue();
403 // double total = 0.0; // only needed if calculating percentages
404 // if (this.renderAsPercentages) {
405 // total = DataUtilities.calculateColumnTotal(dataset, column);
406 // value = value / total;
407 // }
408 //
409 // // leave the y values (y1, y0) untranslated as it is going to be be
410 // // stacked up later by previous series values, after this it will be
411 // // translated.
412 // double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
413 // dataArea, plot.getDomainAxisEdge());
414 //
415 // double previousHeightx1 = getPreviousHeight(dataset, row, column);
416 // double y1 = value + previousHeightx1;
417 // RectangleEdge location = plot.getRangeAxisEdge();
418 // double yy1 = rangeAxis.valueToJava2D(y1, dataArea, location);
419 //
420 // g2.setPaint(getItemPaint(row, column));
421 // g2.setStroke(getItemStroke(row, column));
422 //
423 // // in column zero, the only job to do is draw any visible item labels
424 // // and this is done in the second pass...
425 // if (column == 0) {
426 // if (pass == 1) {
427 // // draw item labels, if visible
428 // if (isItemLabelVisible(row, column)) {
429 // drawItemLabel(g2, plot.getOrientation(), dataset, row, column,
430 // xx1, yy1, (y1 < 0.0));
431 // }
432 // }
433 // }
434 // else {
435 // Number previousValue = dataset.getValue(row, column - 1);
436 // if (previousValue != null) {
437 //
438 // double xx0 = domainAxis.getCategoryMiddle(column - 1,
439 // getColumnCount(), dataArea, plot.getDomainAxisEdge());
440 // double y0 = previousValue.doubleValue();
441 // if (this.renderAsPercentages) {
442 // total = DataUtilities.calculateColumnTotal(dataset,
443 // column - 1);
444 // y0 = y0 / total;
445 // }
446 //
447 //
448 // // Get the previous height, but this will be different for both
449 // // y0 and y1 as the previous series values could differ.
450 // double previousHeightx0 = getPreviousHeight(dataset, row,
451 // column - 1);
452 //
453 // // Now stack the current y values on top of the previous values.
454 // y0 += previousHeightx0;
455 //
456 // // Now translate the previous heights
457 // double previousHeightxx0 = rangeAxis.valueToJava2D(
458 // previousHeightx0, dataArea, location);
459 // double previousHeightxx1 = rangeAxis.valueToJava2D(
460 // previousHeightx1, dataArea, location);
461 //
462 // // Now translate the current y values.
463 // double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location);
464 //
465 // if (pass == 0) {
466 // // FIXME: this doesn't handle negative values properly
467 // Polygon p = null;
468 // PlotOrientation orientation = plot.getOrientation();
469 // if (orientation == PlotOrientation.HORIZONTAL) {
470 // p = new Polygon();
471 // p.addPoint((int) yy0, (int) xx0);
472 // p.addPoint((int) yy1, (int) xx1);
473 // p.addPoint((int) previousHeightxx1, (int) xx1);
474 // p.addPoint((int) previousHeightxx0, (int) xx0);
475 // }
476 // else if (orientation == PlotOrientation.VERTICAL) {
477 // p = new Polygon();
478 // p.addPoint((int) xx0, (int) yy0);
479 // p.addPoint((int) xx1, (int) yy1);
480 // p.addPoint((int) xx1, (int) previousHeightxx1);
481 // p.addPoint((int) xx0, (int) previousHeightxx0);
482 // }
483 // g2.setPaint(getItemPaint(row, column));
484 // g2.setStroke(getItemStroke(row, column));
485 // g2.fill(p);
486 //
487 // // add an item entity, if this information is being
488 // // collected...
489 // EntityCollection entities = state.getEntityCollection();
490 // if (entities != null) {
491 // addItemEntity(entities, dataset, row, column, p);
492 // }
493 //
494 // }
495 // else {
496 // if (isItemLabelVisible(row, column)) {
497 // drawItemLabel(g2, plot.getOrientation(), dataset, row,
498 // column, xx1, yy1, (y1 < 0.0));
499 // }
500 // }
501 // }
502 //
503 //
504 // }
505 //
506 // }
507
508 /**
509 * Calculates the stacked value of the all series up to, but not including
510 * <code>series</code> for the specified category, <code>category</code>.
511 * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
512 *
513 * @param dataset the dataset (<code>null</code> not permitted).
514 * @param series the series.
515 * @param category the category.
516 *
517 * @return double returns a cumulative value for all series' values up to
518 * but excluding <code>series</code> for Object
519 * <code>category</code>.
520 */
521 protected double getPreviousHeight(CategoryDataset dataset,
522 int series, int category) {
523
524 double result = 0.0;
525 Number n;
526 double total = 0.0;
527 if (this.renderAsPercentages) {
528 total = DataUtilities.calculateColumnTotal(dataset, category);
529 }
530 for (int i = 0; i < series; i++) {
531 n = dataset.getValue(i, category);
532 if (n != null) {
533 double v = n.doubleValue();
534 if (this.renderAsPercentages) {
535 v = v / total;
536 }
537 result += v;
538 }
539 }
540 return result;
541
542 }
543
544 /**
545 * Calculates the stacked values (one positive and one negative) of all
546 * series up to, but not including, <code>series</code> for the specified
547 * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
548 *
549 * @param dataset the dataset (<code>null</code> not permitted).
550 * @param series the series index.
551 * @param index the item index.
552 *
553 * @return An array containing the cumulative negative and positive values
554 * for all series values up to but excluding <code>series</code>
555 * for <code>index</code>.
556 */
557 protected double[] getStackValues(CategoryDataset dataset,
558 int series, int index) {
559 double[] result = new double[2];
560 for (int i = 0; i < series; i++) {
561 if (isSeriesVisible(i)) {
562 double v = 0.0;
563 Number n = dataset.getValue(i, index);
564 if (n != null) {
565 v = n.doubleValue();
566 }
567 if (!Double.isNaN(v)) {
568 if (v >= 0.0) {
569 result[1] += v;
570 }
571 else {
572 result[0] += v;
573 }
574 }
575 }
576 }
577 return result;
578 }
579
580 /**
581 * Returns a pair of "stack" values calculated as the mean of the two
582 * specified stack value pairs.
583 *
584 * @param stack1 the first stack pair.
585 * @param stack2 the second stack pair.
586 *
587 * @return A pair of average stack values.
588 */
589 private double[] averageStackValues(double[] stack1, double[] stack2) {
590 double[] result = new double[2];
591 result[0] = (stack1[0] + stack2[0]) / 2.0;
592 result[1] = (stack1[1] + stack2[1]) / 2.0;
593 return result;
594 }
595
596 /**
597 * Calculates adjusted stack values from the supplied values. The value is
598 * the mean of the supplied values, unless either of the supplied values
599 * is zero, in which case the adjusted value is zero also.
600 *
601 * @param stack1 the first stack pair.
602 * @param stack2 the second stack pair.
603 *
604 * @return A pair of average stack values.
605 */
606 private double[] adjustedStackValues(double[] stack1, double[] stack2) {
607 double[] result = new double[2];
608 if (stack1[0] == 0.0 || stack2[0] == 0.0) {
609 result[0] = 0.0;
610 }
611 else {
612 result[0] = (stack1[0] + stack2[0]) / 2.0;
613 }
614 if (stack1[1] == 0.0 || stack2[1] == 0.0) {
615 result[1] = 0.0;
616 }
617 else {
618 result[1] = (stack1[1] + stack2[1]) / 2.0;
619 }
620 return result;
621 }
622
623 /**
624 * Checks this instance for equality with an arbitrary object.
625 *
626 * @param obj the object (<code>null</code> not permitted).
627 *
628 * @return A boolean.
629 */
630 public boolean equals(Object obj) {
631 if (obj == this) {
632 return true;
633 }
634 if (! (obj instanceof StackedAreaRenderer)) {
635 return false;
636 }
637 StackedAreaRenderer that = (StackedAreaRenderer) obj;
638 if (this.renderAsPercentages != that.renderAsPercentages) {
639 return false;
640 }
641 return super.equals(obj);
642 }
643 }