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: * StackedXYAreaRenderer2.java
029: * ---------------------------
030: * (C) Copyright 2004-2006, by Object Refinery Limited and Contributors.
031: *
032: * Original Author: David Gilbert (for Object Refinery Limited), based on
033: * the StackedXYAreaRenderer class by Richard Atkinson;
034: * Contributor(s): -;
035: *
036: * $Id: StackedXYAreaRenderer2.java,v 1.6.2.6 2007/02/06 15:32:14 mungady Exp $
037: *
038: * Changes:
039: * --------
040: * 30-Apr-2004 : Version 1 (DG);
041: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
042: * getYValue() (DG);
043: * 10-Sep-2004 : Removed getRangeType() method (DG);
044: * 06-Jan-2004 : Renamed getRangeExtent() --> findRangeBounds (DG);
045: * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG);
046: * 03-Oct-2005 : Add entity generation to drawItem() method (DG);
047: * ------------- JFREECHART 1.0.x ---------------------------------------------
048: * 22-Aug-2006 : Handle null and empty datasets correctly in the
049: * findRangeBounds() method (DG);
050: * 22-Sep-2006 : Added a flag to allow rounding of x-coordinates (after
051: * translation to Java2D space) in order to avoid the striping
052: * that can result from anti-aliasing (thanks to Doug
053: * Clayton) (DG);
054: * 30-Nov-2006 : Added accessor methods for the roundXCoordinates flag (DG);
055: *
056: */
057:
058: package org.jfree.chart.renderer.xy;
059:
060: import java.awt.Graphics2D;
061: import java.awt.Paint;
062: import java.awt.Shape;
063: import java.awt.geom.GeneralPath;
064: import java.awt.geom.Rectangle2D;
065: import java.io.Serializable;
066:
067: import org.jfree.chart.axis.ValueAxis;
068: import org.jfree.chart.entity.EntityCollection;
069: import org.jfree.chart.event.RendererChangeEvent;
070: import org.jfree.chart.labels.XYToolTipGenerator;
071: import org.jfree.chart.plot.CrosshairState;
072: import org.jfree.chart.plot.PlotRenderingInfo;
073: import org.jfree.chart.plot.XYPlot;
074: import org.jfree.chart.urls.XYURLGenerator;
075: import org.jfree.data.Range;
076: import org.jfree.data.xy.TableXYDataset;
077: import org.jfree.data.xy.XYDataset;
078: import org.jfree.ui.RectangleEdge;
079: import org.jfree.util.PublicCloneable;
080:
081: /**
082: * A stacked area renderer for the {@link XYPlot} class.
083: */
084: public class StackedXYAreaRenderer2 extends XYAreaRenderer2 implements
085: Cloneable, PublicCloneable, Serializable {
086:
087: /** For serialization. */
088: private static final long serialVersionUID = 7752676509764539182L;
089:
090: /**
091: * This flag controls whether or not the x-coordinates (in Java2D space)
092: * are rounded to integers. When set to true, this can avoid the vertical
093: * striping that anti-aliasing can generate. However, the rounding may not
094: * be appropriate for output in high resolution formats (for example,
095: * vector graphics formats such as SVG and PDF).
096: *
097: * @since 1.0.3
098: */
099: private boolean roundXCoordinates;
100:
101: /**
102: * Creates a new renderer.
103: */
104: public StackedXYAreaRenderer2() {
105: this (null, null);
106: }
107:
108: /**
109: * Constructs a new renderer.
110: *
111: * @param labelGenerator the tool tip generator to use. <code>null</code>
112: * is none.
113: * @param urlGenerator the URL generator (<code>null</code> permitted).
114: */
115: public StackedXYAreaRenderer2(XYToolTipGenerator labelGenerator,
116: XYURLGenerator urlGenerator) {
117: super (labelGenerator, urlGenerator);
118: this .roundXCoordinates = true;
119: }
120:
121: /**
122: * Returns the flag that controls whether or not the x-coordinates (in
123: * Java2D space) are rounded to integer values.
124: *
125: * @return The flag.
126: *
127: * @since 1.0.4
128: *
129: * @see #setRoundXCoordinates(boolean)
130: */
131: public boolean getRoundXCoordinates() {
132: return this .roundXCoordinates;
133: }
134:
135: /**
136: * Sets the flag that controls whether or not the x-coordinates (in
137: * Java2D space) are rounded to integer values, and sends a
138: * {@link RendererChangeEvent} to all registered listeners.
139: *
140: * @param round the new flag value.
141: *
142: * @since 1.0.4
143: *
144: * @see #getRoundXCoordinates()
145: */
146: public void setRoundXCoordinates(boolean round) {
147: this .roundXCoordinates = round;
148: notifyListeners(new RendererChangeEvent(this ));
149: }
150:
151: /**
152: * Returns the range of values the renderer requires to display all the
153: * items from the specified dataset.
154: *
155: * @param dataset the dataset (<code>null</code> permitted).
156: *
157: * @return The range (or <code>null</code> if the dataset is
158: * <code>null</code> or empty).
159: */
160: public Range findRangeBounds(XYDataset dataset) {
161: if (dataset == null) {
162: return null;
163: }
164: double min = Double.POSITIVE_INFINITY;
165: double max = Double.NEGATIVE_INFINITY;
166: TableXYDataset d = (TableXYDataset) dataset;
167: int itemCount = d.getItemCount();
168: for (int i = 0; i < itemCount; i++) {
169: double[] stackValues = getStackValues(
170: (TableXYDataset) dataset, d.getSeriesCount(), i);
171: min = Math.min(min, stackValues[0]);
172: max = Math.max(max, stackValues[1]);
173: }
174: if (min == Double.POSITIVE_INFINITY) {
175: return null;
176: }
177: return new Range(min, max);
178: }
179:
180: /**
181: * Returns the number of passes required by the renderer.
182: *
183: * @return 1.
184: */
185: public int getPassCount() {
186: return 1;
187: }
188:
189: /**
190: * Draws the visual representation of a single data item.
191: *
192: * @param g2 the graphics device.
193: * @param state the renderer state.
194: * @param dataArea the area within which the data is being drawn.
195: * @param info collects information about the drawing.
196: * @param plot the plot (can be used to obtain standard color information
197: * etc).
198: * @param domainAxis the domain axis.
199: * @param rangeAxis the range axis.
200: * @param dataset the dataset.
201: * @param series the series index (zero-based).
202: * @param item the item index (zero-based).
203: * @param crosshairState information about crosshairs on a plot.
204: * @param pass the pass index.
205: */
206: public void drawItem(Graphics2D g2, XYItemRendererState state,
207: Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
208: ValueAxis domainAxis, ValueAxis rangeAxis,
209: XYDataset dataset, int series, int item,
210: CrosshairState crosshairState, int pass) {
211:
212: // setup for collecting optional entity info...
213: Shape entityArea = null;
214: EntityCollection entities = null;
215: if (info != null) {
216: entities = info.getOwner().getEntityCollection();
217: }
218:
219: TableXYDataset tdataset = (TableXYDataset) dataset;
220:
221: // get the data point...
222: double x1 = dataset.getXValue(series, item);
223: double y1 = dataset.getYValue(series, item);
224: if (Double.isNaN(y1)) {
225: y1 = 0.0;
226: }
227: double[] stack1 = getStackValues(tdataset, series, item);
228:
229: // get the previous point and the next point so we can calculate a
230: // "hot spot" for the area (used by the chart entity)...
231: double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
232: double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
233: if (Double.isNaN(y0)) {
234: y0 = 0.0;
235: }
236: double[] stack0 = getStackValues(tdataset, series, Math.max(
237: item - 1, 0));
238:
239: int itemCount = dataset.getItemCount(series);
240: double x2 = dataset.getXValue(series, Math.min(item + 1,
241: itemCount - 1));
242: double y2 = dataset.getYValue(series, Math.min(item + 1,
243: itemCount - 1));
244: if (Double.isNaN(y2)) {
245: y2 = 0.0;
246: }
247: double[] stack2 = getStackValues(tdataset, series, Math.min(
248: item + 1, itemCount - 1));
249:
250: double xleft = (x0 + x1) / 2.0;
251: double xright = (x1 + x2) / 2.0;
252: double[] stackLeft = averageStackValues(stack0, stack1);
253: double[] stackRight = averageStackValues(stack1, stack2);
254: double[] adjStackLeft = adjustedStackValues(stack0, stack1);
255: double[] adjStackRight = adjustedStackValues(stack1, stack2);
256:
257: RectangleEdge edge0 = plot.getDomainAxisEdge();
258:
259: float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea,
260: edge0);
261: float transXLeft = (float) domainAxis.valueToJava2D(xleft,
262: dataArea, edge0);
263: float transXRight = (float) domainAxis.valueToJava2D(xright,
264: dataArea, edge0);
265:
266: if (this .roundXCoordinates) {
267: transX1 = Math.round(transX1);
268: transXLeft = Math.round(transXLeft);
269: transXRight = Math.round(transXRight);
270: }
271: float transY1;
272:
273: RectangleEdge edge1 = plot.getRangeAxisEdge();
274:
275: GeneralPath left = new GeneralPath();
276: GeneralPath right = new GeneralPath();
277: if (y1 >= 0.0) { // handle positive value
278: transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1],
279: dataArea, edge1);
280: float transStack1 = (float) rangeAxis.valueToJava2D(
281: stack1[1], dataArea, edge1);
282: float transStackLeft = (float) rangeAxis.valueToJava2D(
283: adjStackLeft[1], dataArea, edge1);
284:
285: // LEFT POLYGON
286: if (y0 >= 0.0) {
287: double yleft = (y0 + y1) / 2.0 + stackLeft[1];
288: float transYLeft = (float) rangeAxis.valueToJava2D(
289: yleft, dataArea, edge1);
290: left.moveTo(transX1, transY1);
291: left.lineTo(transX1, transStack1);
292: left.lineTo(transXLeft, transStackLeft);
293: left.lineTo(transXLeft, transYLeft);
294: left.closePath();
295: } else {
296: left.moveTo(transX1, transStack1);
297: left.lineTo(transX1, transY1);
298: left.lineTo(transXLeft, transStackLeft);
299: left.closePath();
300: }
301:
302: float transStackRight = (float) rangeAxis.valueToJava2D(
303: adjStackRight[1], dataArea, edge1);
304: // RIGHT POLYGON
305: if (y2 >= 0.0) {
306: double yright = (y1 + y2) / 2.0 + stackRight[1];
307: float transYRight = (float) rangeAxis.valueToJava2D(
308: yright, dataArea, edge1);
309: right.moveTo(transX1, transStack1);
310: right.lineTo(transX1, transY1);
311: right.lineTo(transXRight, transYRight);
312: right.lineTo(transXRight, transStackRight);
313: right.closePath();
314: } else {
315: right.moveTo(transX1, transStack1);
316: right.lineTo(transX1, transY1);
317: right.lineTo(transXRight, transStackRight);
318: right.closePath();
319: }
320: } else { // handle negative value
321: transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0],
322: dataArea, edge1);
323: float transStack1 = (float) rangeAxis.valueToJava2D(
324: stack1[0], dataArea, edge1);
325: float transStackLeft = (float) rangeAxis.valueToJava2D(
326: adjStackLeft[0], dataArea, edge1);
327:
328: // LEFT POLYGON
329: if (y0 >= 0.0) {
330: left.moveTo(transX1, transStack1);
331: left.lineTo(transX1, transY1);
332: left.lineTo(transXLeft, transStackLeft);
333: left.clone();
334: } else {
335: double yleft = (y0 + y1) / 2.0 + stackLeft[0];
336: float transYLeft = (float) rangeAxis.valueToJava2D(
337: yleft, dataArea, edge1);
338: left.moveTo(transX1, transY1);
339: left.lineTo(transX1, transStack1);
340: left.lineTo(transXLeft, transStackLeft);
341: left.lineTo(transXLeft, transYLeft);
342: left.closePath();
343: }
344: float transStackRight = (float) rangeAxis.valueToJava2D(
345: adjStackRight[0], dataArea, edge1);
346:
347: // RIGHT POLYGON
348: if (y2 >= 0.0) {
349: right.moveTo(transX1, transStack1);
350: right.lineTo(transX1, transY1);
351: right.lineTo(transXRight, transStackRight);
352: right.closePath();
353: } else {
354: double yright = (y1 + y2) / 2.0 + stackRight[0];
355: float transYRight = (float) rangeAxis.valueToJava2D(
356: yright, dataArea, edge1);
357: right.moveTo(transX1, transStack1);
358: right.lineTo(transX1, transY1);
359: right.lineTo(transXRight, transYRight);
360: right.lineTo(transXRight, transStackRight);
361: right.closePath();
362: }
363: }
364:
365: // Get series Paint and Stroke
366: Paint itemPaint = getItemPaint(series, item);
367: if (pass == 0) {
368: g2.setPaint(itemPaint);
369: g2.fill(left);
370: g2.fill(right);
371: }
372:
373: // add an entity for the item...
374: if (entities != null) {
375: GeneralPath gp = new GeneralPath(left);
376: gp.append(right, false);
377: entityArea = gp;
378: addEntity(entities, entityArea, dataset, series, item,
379: transX1, transY1);
380: }
381:
382: }
383:
384: /**
385: * Calculates the stacked values (one positive and one negative) of all
386: * series up to, but not including, <code>series</code> for the specified
387: * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
388: *
389: * @param dataset the dataset (<code>null</code> not permitted).
390: * @param series the series index.
391: * @param index the item index.
392: *
393: * @return An array containing the cumulative negative and positive values
394: * for all series values up to but excluding <code>series</code>
395: * for <code>index</code>.
396: */
397: private double[] getStackValues(TableXYDataset dataset, int series,
398: int index) {
399: double[] result = new double[2];
400: for (int i = 0; i < series; i++) {
401: double v = dataset.getYValue(i, index);
402: if (!Double.isNaN(v)) {
403: if (v >= 0.0) {
404: result[1] += v;
405: } else {
406: result[0] += v;
407: }
408: }
409: }
410: return result;
411: }
412:
413: /**
414: * Returns a pair of "stack" values calculated as the mean of the two
415: * specified stack value pairs.
416: *
417: * @param stack1 the first stack pair.
418: * @param stack2 the second stack pair.
419: *
420: * @return A pair of average stack values.
421: */
422: private double[] averageStackValues(double[] stack1, double[] stack2) {
423: double[] result = new double[2];
424: result[0] = (stack1[0] + stack2[0]) / 2.0;
425: result[1] = (stack1[1] + stack2[1]) / 2.0;
426: return result;
427: }
428:
429: /**
430: * Calculates adjusted stack values from the supplied values. The value is
431: * the mean of the supplied values, unless either of the supplied values
432: * is zero, in which case the adjusted value is zero also.
433: *
434: * @param stack1 the first stack pair.
435: * @param stack2 the second stack pair.
436: *
437: * @return A pair of average stack values.
438: */
439: private double[] adjustedStackValues(double[] stack1,
440: double[] stack2) {
441: double[] result = new double[2];
442: if (stack1[0] == 0.0 || stack2[0] == 0.0) {
443: result[0] = 0.0;
444: } else {
445: result[0] = (stack1[0] + stack2[0]) / 2.0;
446: }
447: if (stack1[1] == 0.0 || stack2[1] == 0.0) {
448: result[1] = 0.0;
449: } else {
450: result[1] = (stack1[1] + stack2[1]) / 2.0;
451: }
452: return result;
453: }
454:
455: /**
456: * Tests this renderer for equality with an arbitrary object.
457: *
458: * @param obj the object (<code>null</code> permitted).
459: *
460: * @return A boolean.
461: */
462: public boolean equals(Object obj) {
463: if (obj == this ) {
464: return true;
465: }
466: if (!(obj instanceof StackedXYAreaRenderer2)) {
467: return false;
468: }
469: StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj;
470: if (this .roundXCoordinates != that.roundXCoordinates) {
471: return false;
472: }
473: return super .equals(obj);
474: }
475:
476: /**
477: * Returns a clone of the renderer.
478: *
479: * @return A clone.
480: *
481: * @throws CloneNotSupportedException if the renderer cannot be cloned.
482: */
483: public Object clone() throws CloneNotSupportedException {
484: return super.clone();
485: }
486:
487: }
|