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 implements
091: Cloneable, PublicCloneable, Serializable {
092:
093: /** For serialization. */
094: private static final long serialVersionUID = -3595635038460823663L;
095:
096: /** A flag that controls whether the areas display values or percentages. */
097: private boolean renderAsPercentages;
098:
099: /**
100: * Creates a new renderer.
101: */
102: public StackedAreaRenderer() {
103: this (false);
104: }
105:
106: /**
107: * Creates a new renderer.
108: *
109: * @param renderAsPercentages a flag that controls whether the data values
110: * are rendered as percentages.
111: */
112: public StackedAreaRenderer(boolean renderAsPercentages) {
113: super ();
114: this .renderAsPercentages = renderAsPercentages;
115: }
116:
117: /**
118: * Returns <code>true</code> if the renderer displays each item value as
119: * a percentage (so that the stacked areas add to 100%), and
120: * <code>false</code> otherwise.
121: *
122: * @return A boolean.
123: *
124: * @since 1.0.3
125: */
126: public boolean getRenderAsPercentages() {
127: return this .renderAsPercentages;
128: }
129:
130: /**
131: * Sets the flag that controls whether the renderer displays each item
132: * value as a percentage (so that the stacked areas add to 100%), and sends
133: * a {@link RendererChangeEvent} to all registered listeners.
134: *
135: * @param asPercentages the flag.
136: *
137: * @since 1.0.3
138: */
139: public void setRenderAsPercentages(boolean asPercentages) {
140: this .renderAsPercentages = asPercentages;
141: notifyListeners(new RendererChangeEvent(this ));
142: }
143:
144: /**
145: * Returns the number of passes (<code>2</code>) required by this renderer.
146: * The first pass is used to draw the bars, the second pass is used to
147: * draw the item labels (if visible).
148: *
149: * @return The number of passes required by the renderer.
150: */
151: public int getPassCount() {
152: return 2;
153: }
154:
155: /**
156: * Returns the range of values the renderer requires to display all the
157: * items from the specified dataset.
158: *
159: * @param dataset the dataset (<code>null</code> not permitted).
160: *
161: * @return The range (or <code>null</code> if the dataset is empty).
162: */
163: public Range findRangeBounds(CategoryDataset dataset) {
164: if (this .renderAsPercentages) {
165: return new Range(0.0, 1.0);
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, Rectangle2D dataArea,
187: CategoryPlot plot, CategoryAxis domainAxis,
188: ValueAxis rangeAxis, CategoryDataset dataset, int row,
189: int column, int pass) {
190:
191: // setup for collecting optional entity info...
192: Shape entityArea = null;
193: EntityCollection entities = state.getEntityCollection();
194:
195: double y1 = 0.0;
196: Number n = dataset.getValue(row, column);
197: if (n != null) {
198: y1 = n.doubleValue();
199: }
200: double[] stack1 = getStackValues(dataset, row, column);
201:
202: // leave the y values (y1, y0) untranslated as it is going to be be
203: // stacked up later by previous series values, after this it will be
204: // translated.
205: double xx1 = domainAxis.getCategoryMiddle(column,
206: getColumnCount(), dataArea, plot.getDomainAxisEdge());
207:
208: // get the previous point and the next point so we can calculate a
209: // "hot spot" for the area (used by the chart entity)...
210: double y0 = 0.0;
211: n = dataset.getValue(row, Math.max(column - 1, 0));
212: if (n != null) {
213: y0 = n.doubleValue();
214: }
215: double[] stack0 = getStackValues(dataset, row, Math.max(
216: column - 1, 0));
217:
218: // FIXME: calculate xx0
219: double xx0 = domainAxis.getCategoryStart(column,
220: getColumnCount(), dataArea, plot.getDomainAxisEdge());
221:
222: int itemCount = dataset.getColumnCount();
223: double y2 = 0.0;
224: n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
225: if (n != null) {
226: y2 = n.doubleValue();
227: }
228: double[] stack2 = getStackValues(dataset, row, Math.min(
229: column + 1, itemCount - 1));
230:
231: double xx2 = domainAxis.getCategoryEnd(column,
232: getColumnCount(), dataArea, plot.getDomainAxisEdge());
233:
234: // FIXME: calculate xxLeft and xxRight
235: double xxLeft = xx0;
236: double xxRight = xx2;
237:
238: double[] stackLeft = averageStackValues(stack0, stack1);
239: double[] stackRight = averageStackValues(stack1, stack2);
240: double[] adjStackLeft = adjustedStackValues(stack0, stack1);
241: double[] adjStackRight = adjustedStackValues(stack1, stack2);
242:
243: float transY1;
244:
245: RectangleEdge edge1 = plot.getRangeAxisEdge();
246:
247: GeneralPath left = new GeneralPath();
248: GeneralPath right = new GeneralPath();
249: if (y1 >= 0.0) { // handle positive value
250: transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1],
251: dataArea, edge1);
252: float transStack1 = (float) rangeAxis.valueToJava2D(
253: stack1[1], dataArea, edge1);
254: float transStackLeft = (float) rangeAxis.valueToJava2D(
255: adjStackLeft[1], dataArea, edge1);
256:
257: // LEFT POLYGON
258: if (y0 >= 0.0) {
259: double yleft = (y0 + y1) / 2.0 + stackLeft[1];
260: float transYLeft = (float) rangeAxis.valueToJava2D(
261: yleft, dataArea, edge1);
262: left.moveTo((float) xx1, transY1);
263: left.lineTo((float) xx1, transStack1);
264: left.lineTo((float) xxLeft, transStackLeft);
265: left.lineTo((float) xxLeft, transYLeft);
266: left.closePath();
267: } else {
268: left.moveTo((float) xx1, transStack1);
269: left.lineTo((float) xx1, transY1);
270: left.lineTo((float) xxLeft, transStackLeft);
271: left.closePath();
272: }
273:
274: float transStackRight = (float) rangeAxis.valueToJava2D(
275: adjStackRight[1], dataArea, edge1);
276: // RIGHT POLYGON
277: if (y2 >= 0.0) {
278: double yright = (y1 + y2) / 2.0 + stackRight[1];
279: float transYRight = (float) rangeAxis.valueToJava2D(
280: yright, dataArea, edge1);
281: right.moveTo((float) xx1, transStack1);
282: right.lineTo((float) xx1, transY1);
283: right.lineTo((float) xxRight, transYRight);
284: right.lineTo((float) xxRight, transStackRight);
285: right.closePath();
286: } else {
287: right.moveTo((float) xx1, transStack1);
288: right.lineTo((float) xx1, transY1);
289: right.lineTo((float) xxRight, transStackRight);
290: right.closePath();
291: }
292: } else { // handle negative value
293: transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0],
294: dataArea, edge1);
295: float transStack1 = (float) rangeAxis.valueToJava2D(
296: stack1[0], dataArea, edge1);
297: float transStackLeft = (float) rangeAxis.valueToJava2D(
298: adjStackLeft[0], dataArea, edge1);
299:
300: // LEFT POLYGON
301: if (y0 >= 0.0) {
302: left.moveTo((float) xx1, transStack1);
303: left.lineTo((float) xx1, transY1);
304: left.lineTo((float) xxLeft, transStackLeft);
305: left.clone();
306: } else {
307: double yleft = (y0 + y1) / 2.0 + stackLeft[0];
308: float transYLeft = (float) rangeAxis.valueToJava2D(
309: yleft, dataArea, edge1);
310: left.moveTo((float) xx1, transY1);
311: left.lineTo((float) xx1, transStack1);
312: left.lineTo((float) xxLeft, transStackLeft);
313: left.lineTo((float) xxLeft, transYLeft);
314: left.closePath();
315: }
316: float transStackRight = (float) rangeAxis.valueToJava2D(
317: adjStackRight[0], dataArea, edge1);
318:
319: // RIGHT POLYGON
320: if (y2 >= 0.0) {
321: right.moveTo((float) xx1, transStack1);
322: right.lineTo((float) xx1, transY1);
323: right.lineTo((float) xxRight, transStackRight);
324: right.closePath();
325: } else {
326: double yright = (y1 + y2) / 2.0 + stackRight[0];
327: float transYRight = (float) rangeAxis.valueToJava2D(
328: yright, dataArea, edge1);
329: right.moveTo((float) xx1, transStack1);
330: right.lineTo((float) xx1, transY1);
331: right.lineTo((float) xxRight, transYRight);
332: right.lineTo((float) xxRight, transStackRight);
333: right.closePath();
334: }
335: }
336:
337: g2.setPaint(getItemPaint(row, column));
338: g2.setStroke(getItemStroke(row, column));
339:
340: // Get series Paint and Stroke
341: Paint itemPaint = getItemPaint(row, column);
342: if (pass == 0) {
343: g2.setPaint(itemPaint);
344: g2.fill(left);
345: g2.fill(right);
346: }
347:
348: // add an entity for the item...
349: if (entities != null) {
350: GeneralPath gp = new GeneralPath(left);
351: gp.append(right, false);
352: entityArea = gp;
353: addItemEntity(entities, dataset, row, column, entityArea);
354: }
355:
356: }
357:
358: // /**
359: // * Draw a single data item.
360: // *
361: // * @param g2 the graphics device.
362: // * @param state the renderer state.
363: // * @param dataArea the data plot area.
364: // * @param plot the plot.
365: // * @param domainAxis the domain axis.
366: // * @param rangeAxis the range axis.
367: // * @param dataset the data.
368: // * @param row the row index (zero-based).
369: // * @param column the column index (zero-based).
370: // * @param pass the pass index.
371: // */
372: // public void drawItem(Graphics2D g2,
373: // CategoryItemRendererState state,
374: // Rectangle2D dataArea,
375: // CategoryPlot plot,
376: // CategoryAxis domainAxis,
377: // ValueAxis rangeAxis,
378: // CategoryDataset dataset,
379: // int row,
380: // int column,
381: // int pass) {
382: //
383: // // plot non-null values...
384: // Number dataValue = dataset.getValue(row, column);
385: // if (dataValue == null) {
386: // return;
387: // }
388: //
389: // double value = dataValue.doubleValue();
390: // double total = 0.0; // only needed if calculating percentages
391: // if (this.renderAsPercentages) {
392: // total = DataUtilities.calculateColumnTotal(dataset, column);
393: // value = value / total;
394: // }
395: //
396: // // leave the y values (y1, y0) untranslated as it is going to be be
397: // // stacked up later by previous series values, after this it will be
398: // // translated.
399: // double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
400: // dataArea, plot.getDomainAxisEdge());
401: //
402: // double previousHeightx1 = getPreviousHeight(dataset, row, column);
403: // double y1 = value + previousHeightx1;
404: // RectangleEdge location = plot.getRangeAxisEdge();
405: // double yy1 = rangeAxis.valueToJava2D(y1, dataArea, location);
406: //
407: // g2.setPaint(getItemPaint(row, column));
408: // g2.setStroke(getItemStroke(row, column));
409: //
410: // // in column zero, the only job to do is draw any visible item labels
411: // // and this is done in the second pass...
412: // if (column == 0) {
413: // if (pass == 1) {
414: // // draw item labels, if visible
415: // if (isItemLabelVisible(row, column)) {
416: // drawItemLabel(g2, plot.getOrientation(), dataset, row, column,
417: // xx1, yy1, (y1 < 0.0));
418: // }
419: // }
420: // }
421: // else {
422: // Number previousValue = dataset.getValue(row, column - 1);
423: // if (previousValue != null) {
424: //
425: // double xx0 = domainAxis.getCategoryMiddle(column - 1,
426: // getColumnCount(), dataArea, plot.getDomainAxisEdge());
427: // double y0 = previousValue.doubleValue();
428: // if (this.renderAsPercentages) {
429: // total = DataUtilities.calculateColumnTotal(dataset,
430: // column - 1);
431: // y0 = y0 / total;
432: // }
433: //
434: //
435: // // Get the previous height, but this will be different for both
436: // // y0 and y1 as the previous series values could differ.
437: // double previousHeightx0 = getPreviousHeight(dataset, row,
438: // column - 1);
439: //
440: // // Now stack the current y values on top of the previous values.
441: // y0 += previousHeightx0;
442: //
443: // // Now translate the previous heights
444: // double previousHeightxx0 = rangeAxis.valueToJava2D(
445: // previousHeightx0, dataArea, location);
446: // double previousHeightxx1 = rangeAxis.valueToJava2D(
447: // previousHeightx1, dataArea, location);
448: //
449: // // Now translate the current y values.
450: // double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location);
451: //
452: // if (pass == 0) {
453: // // FIXME: this doesn't handle negative values properly
454: // Polygon p = null;
455: // PlotOrientation orientation = plot.getOrientation();
456: // if (orientation == PlotOrientation.HORIZONTAL) {
457: // p = new Polygon();
458: // p.addPoint((int) yy0, (int) xx0);
459: // p.addPoint((int) yy1, (int) xx1);
460: // p.addPoint((int) previousHeightxx1, (int) xx1);
461: // p.addPoint((int) previousHeightxx0, (int) xx0);
462: // }
463: // else if (orientation == PlotOrientation.VERTICAL) {
464: // p = new Polygon();
465: // p.addPoint((int) xx0, (int) yy0);
466: // p.addPoint((int) xx1, (int) yy1);
467: // p.addPoint((int) xx1, (int) previousHeightxx1);
468: // p.addPoint((int) xx0, (int) previousHeightxx0);
469: // }
470: // g2.setPaint(getItemPaint(row, column));
471: // g2.setStroke(getItemStroke(row, column));
472: // g2.fill(p);
473: //
474: // // add an item entity, if this information is being
475: // // collected...
476: // EntityCollection entities = state.getEntityCollection();
477: // if (entities != null) {
478: // addItemEntity(entities, dataset, row, column, p);
479: // }
480: //
481: // }
482: // else {
483: // if (isItemLabelVisible(row, column)) {
484: // drawItemLabel(g2, plot.getOrientation(), dataset, row,
485: // column, xx1, yy1, (y1 < 0.0));
486: // }
487: // }
488: // }
489: //
490: //
491: // }
492: //
493: // }
494:
495: /**
496: * Calculates the stacked value of the all series up to, but not including
497: * <code>series</code> for the specified category, <code>category</code>.
498: * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
499: *
500: * @param dataset the dataset (<code>null</code> not permitted).
501: * @param series the series.
502: * @param category the category.
503: *
504: * @return double returns a cumulative value for all series' values up to
505: * but excluding <code>series</code> for Object
506: * <code>category</code>.
507: */
508: protected double getPreviousHeight(CategoryDataset dataset,
509: int series, int category) {
510:
511: double result = 0.0;
512: Number n;
513: double total = 0.0;
514: if (this .renderAsPercentages) {
515: total = DataUtilities.calculateColumnTotal(dataset,
516: category);
517: }
518: for (int i = 0; i < series; i++) {
519: n = dataset.getValue(i, category);
520: if (n != null) {
521: double v = n.doubleValue();
522: if (this .renderAsPercentages) {
523: v = v / total;
524: }
525: result += v;
526: }
527: }
528: return result;
529:
530: }
531:
532: /**
533: * Calculates the stacked values (one positive and one negative) of all
534: * series up to, but not including, <code>series</code> for the specified
535: * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
536: *
537: * @param dataset the dataset (<code>null</code> not permitted).
538: * @param series the series index.
539: * @param index the item index.
540: *
541: * @return An array containing the cumulative negative and positive values
542: * for all series values up to but excluding <code>series</code>
543: * for <code>index</code>.
544: */
545: protected double[] getStackValues(CategoryDataset dataset,
546: int series, int index) {
547: double[] result = new double[2];
548: for (int i = 0; i < series; i++) {
549: if (isSeriesVisible(i)) {
550: double v = 0.0;
551: Number n = dataset.getValue(i, index);
552: if (n != null) {
553: v = n.doubleValue();
554: }
555: if (!Double.isNaN(v)) {
556: if (v >= 0.0) {
557: result[1] += v;
558: } else {
559: result[0] += v;
560: }
561: }
562: }
563: }
564: return result;
565: }
566:
567: /**
568: * Returns a pair of "stack" values calculated as the mean of the two
569: * specified stack value pairs.
570: *
571: * @param stack1 the first stack pair.
572: * @param stack2 the second stack pair.
573: *
574: * @return A pair of average stack values.
575: */
576: private double[] averageStackValues(double[] stack1, double[] stack2) {
577: double[] result = new double[2];
578: result[0] = (stack1[0] + stack2[0]) / 2.0;
579: result[1] = (stack1[1] + stack2[1]) / 2.0;
580: return result;
581: }
582:
583: /**
584: * Calculates adjusted stack values from the supplied values. The value is
585: * the mean of the supplied values, unless either of the supplied values
586: * is zero, in which case the adjusted value is zero also.
587: *
588: * @param stack1 the first stack pair.
589: * @param stack2 the second stack pair.
590: *
591: * @return A pair of average stack values.
592: */
593: private double[] adjustedStackValues(double[] stack1,
594: double[] stack2) {
595: double[] result = new double[2];
596: if (stack1[0] == 0.0 || stack2[0] == 0.0) {
597: result[0] = 0.0;
598: } else {
599: result[0] = (stack1[0] + stack2[0]) / 2.0;
600: }
601: if (stack1[1] == 0.0 || stack2[1] == 0.0) {
602: result[1] = 0.0;
603: } else {
604: result[1] = (stack1[1] + stack2[1]) / 2.0;
605: }
606: return result;
607: }
608:
609: /**
610: * Checks this instance for equality with an arbitrary object.
611: *
612: * @param obj the object (<code>null</code> not permitted).
613: *
614: * @return A boolean.
615: */
616: public boolean equals(Object obj) {
617: if (obj == this ) {
618: return true;
619: }
620: if (!(obj instanceof StackedAreaRenderer)) {
621: return false;
622: }
623: StackedAreaRenderer that = (StackedAreaRenderer) obj;
624: if (this .renderAsPercentages != that.renderAsPercentages) {
625: return false;
626: }
627: return super.equals(obj);
628: }
629: }
|