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: * StackedXYAreaRenderer.java
029: * --------------------------
030: * (C) Copyright 2003-2007, by Richard Atkinson and Contributors.
031: *
032: * Original Author: Richard Atkinson;
033: * Contributor(s): Christian W. Zuckschwerdt;
034: * David Gilbert (for Object Refinery Limited);
035: *
036: * $Id: StackedXYAreaRenderer.java,v 1.12.2.13 2007/05/24 13:49:12 mungady Exp $
037: *
038: * Changes:
039: * --------
040: * 27-Jul-2003 : Initial version (RA);
041: * 30-Jul-2003 : Modified entity constructor (CZ);
042: * 18-Aug-2003 : Now handles null values (RA);
043: * 20-Aug-2003 : Implemented Cloneable, PublicCloneable and Serializable (DG);
044: * 22-Sep-2003 : Changed to be a two pass renderer with optional shape Paint
045: * and Stroke (RA);
046: * 07-Oct-2003 : Added renderer state (DG);
047: * 10-Feb-2004 : Updated state object and changed drawItem() method to make
048: * overriding easier (DG);
049: * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed
050: * XYToolTipGenerator --> XYItemLabelGenerator (DG);
051: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
052: * getYValue() (DG);
053: * 10-Sep-2004 : Removed getRangeType() method (DG);
054: * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
055: * 06-Jan-2005 : Override equals() (DG);
056: * 07-Jan-2005 : Update for method name changes in DatasetUtilities (DG);
057: * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG);
058: * 06-Jun-2005 : Fixed null pointer exception, plus problems with equals() and
059: * serialization (DG);
060: * ------------- JFREECHART 1.0.x ---------------------------------------------
061: * 10-Nov-2006 : Fixed bug 1593156, NullPointerException with line
062: * plotting (DG);
063: * 02-Feb-2007 : Fixed bug 1649686, crosshairs don't stack y-values (DG);
064: * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
065: * 22-Mar-2007 : Fire change events in setShapePaint() and setShapeStroke()
066: * methods (DG);
067: * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
068: *
069: */
070:
071: package org.jfree.chart.renderer.xy;
072:
073: import java.awt.Graphics2D;
074: import java.awt.Paint;
075: import java.awt.Point;
076: import java.awt.Polygon;
077: import java.awt.Shape;
078: import java.awt.Stroke;
079: import java.awt.geom.Line2D;
080: import java.awt.geom.Rectangle2D;
081: import java.io.IOException;
082: import java.io.ObjectInputStream;
083: import java.io.ObjectOutputStream;
084: import java.io.Serializable;
085: import java.util.Stack;
086:
087: import org.jfree.chart.axis.ValueAxis;
088: import org.jfree.chart.entity.EntityCollection;
089: import org.jfree.chart.entity.XYItemEntity;
090: import org.jfree.chart.event.RendererChangeEvent;
091: import org.jfree.chart.labels.XYToolTipGenerator;
092: import org.jfree.chart.plot.CrosshairState;
093: import org.jfree.chart.plot.PlotOrientation;
094: import org.jfree.chart.plot.PlotRenderingInfo;
095: import org.jfree.chart.plot.XYPlot;
096: import org.jfree.chart.urls.XYURLGenerator;
097: import org.jfree.data.Range;
098: import org.jfree.data.general.DatasetUtilities;
099: import org.jfree.data.xy.TableXYDataset;
100: import org.jfree.data.xy.XYDataset;
101: import org.jfree.io.SerialUtilities;
102: import org.jfree.util.ObjectUtilities;
103: import org.jfree.util.PaintUtilities;
104: import org.jfree.util.PublicCloneable;
105: import org.jfree.util.ShapeUtilities;
106:
107: /**
108: * A stacked area renderer for the {@link XYPlot} class.
109: * <br><br>
110: * SPECIAL NOTE: This renderer does not currently handle negative data values
111: * correctly. This should get fixed at some point, but the current workaround
112: * is to use the {@link StackedXYAreaRenderer2} class instead.
113: */
114: public class StackedXYAreaRenderer extends XYAreaRenderer implements
115: Cloneable, PublicCloneable, Serializable {
116:
117: /** For serialization. */
118: private static final long serialVersionUID = 5217394318178570889L;
119:
120: /**
121: * A state object for use by this renderer.
122: */
123: static class StackedXYAreaRendererState extends XYItemRendererState {
124:
125: /** The area for the current series. */
126: private Polygon seriesArea;
127:
128: /** The line. */
129: private Line2D line;
130:
131: /** The points from the last series. */
132: private Stack lastSeriesPoints;
133:
134: /** The points for the current series. */
135: private Stack currentSeriesPoints;
136:
137: /**
138: * Creates a new state for the renderer.
139: *
140: * @param info the plot rendering info.
141: */
142: public StackedXYAreaRendererState(PlotRenderingInfo info) {
143: super (info);
144: this .seriesArea = null;
145: this .line = new Line2D.Double();
146: this .lastSeriesPoints = new Stack();
147: this .currentSeriesPoints = new Stack();
148: }
149:
150: /**
151: * Returns the series area.
152: *
153: * @return The series area.
154: */
155: public Polygon getSeriesArea() {
156: return this .seriesArea;
157: }
158:
159: /**
160: * Sets the series area.
161: *
162: * @param area the area.
163: */
164: public void setSeriesArea(Polygon area) {
165: this .seriesArea = area;
166: }
167:
168: /**
169: * Returns the working line.
170: *
171: * @return The working line.
172: */
173: public Line2D getLine() {
174: return this .line;
175: }
176:
177: /**
178: * Returns the current series points.
179: *
180: * @return The current series points.
181: */
182: public Stack getCurrentSeriesPoints() {
183: return this .currentSeriesPoints;
184: }
185:
186: /**
187: * Sets the current series points.
188: *
189: * @param points the points.
190: */
191: public void setCurrentSeriesPoints(Stack points) {
192: this .currentSeriesPoints = points;
193: }
194:
195: /**
196: * Returns the last series points.
197: *
198: * @return The last series points.
199: */
200: public Stack getLastSeriesPoints() {
201: return this .lastSeriesPoints;
202: }
203:
204: /**
205: * Sets the last series points.
206: *
207: * @param points the points.
208: */
209: public void setLastSeriesPoints(Stack points) {
210: this .lastSeriesPoints = points;
211: }
212:
213: }
214:
215: /**
216: * Custom Paint for drawing all shapes, if null defaults to series shapes
217: */
218: private transient Paint shapePaint = null;
219:
220: /**
221: * Custom Stroke for drawing all shapes, if null defaults to series
222: * strokes.
223: */
224: private transient Stroke shapeStroke = null;
225:
226: /**
227: * Creates a new renderer.
228: */
229: public StackedXYAreaRenderer() {
230: this (AREA);
231: }
232:
233: /**
234: * Constructs a new renderer.
235: *
236: * @param type the type of the renderer.
237: */
238: public StackedXYAreaRenderer(int type) {
239: this (type, null, null);
240: }
241:
242: /**
243: * Constructs a new renderer. To specify the type of renderer, use one of
244: * the constants: <code>SHAPES</code>, <code>LINES</code>,
245: * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or
246: * <code>AREA_AND_SHAPES</code>.
247: *
248: * @param type the type of renderer.
249: * @param labelGenerator the tool tip generator to use (<code>null</code>
250: * is none).
251: * @param urlGenerator the URL generator (<code>null</code> permitted).
252: */
253: public StackedXYAreaRenderer(int type,
254: XYToolTipGenerator labelGenerator,
255: XYURLGenerator urlGenerator) {
256:
257: super (type, labelGenerator, urlGenerator);
258: }
259:
260: /**
261: * Returns the paint used for rendering shapes, or <code>null</code> if
262: * using series paints.
263: *
264: * @return The paint (possibly <code>null</code>).
265: *
266: * @see #setShapePaint(Paint)
267: */
268: public Paint getShapePaint() {
269: return this .shapePaint;
270: }
271:
272: /**
273: * Sets the paint for rendering shapes and sends a
274: * {@link RendererChangeEvent} to all registered listeners.
275: *
276: * @param shapePaint the paint (<code>null</code> permitted).
277: *
278: * @see #getShapePaint()
279: */
280: public void setShapePaint(Paint shapePaint) {
281: this .shapePaint = shapePaint;
282: fireChangeEvent();
283: }
284:
285: /**
286: * Returns the stroke used for rendering shapes, or <code>null</code> if
287: * using series strokes.
288: *
289: * @return The stroke (possibly <code>null</code>).
290: *
291: * @see #setShapeStroke(Stroke)
292: */
293: public Stroke getShapeStroke() {
294: return this .shapeStroke;
295: }
296:
297: /**
298: * Sets the stroke for rendering shapes and sends a
299: * {@link RendererChangeEvent} to all registered listeners.
300: *
301: * @param shapeStroke the stroke (<code>null</code> permitted).
302: *
303: * @see #getShapeStroke()
304: */
305: public void setShapeStroke(Stroke shapeStroke) {
306: this .shapeStroke = shapeStroke;
307: fireChangeEvent();
308: }
309:
310: /**
311: * Initialises the renderer. This method will be called before the first
312: * item is rendered, giving the renderer an opportunity to initialise any
313: * state information it wants to maintain.
314: *
315: * @param g2 the graphics device.
316: * @param dataArea the area inside the axes.
317: * @param plot the plot.
318: * @param data the data.
319: * @param info an optional info collection object to return data back to
320: * the caller.
321: *
322: * @return A state object that should be passed to subsequent calls to the
323: * drawItem() method.
324: */
325: public XYItemRendererState initialise(Graphics2D g2,
326: Rectangle2D dataArea, XYPlot plot, XYDataset data,
327: PlotRenderingInfo info) {
328:
329: XYItemRendererState state = new StackedXYAreaRendererState(info);
330: // in the rendering process, there is special handling for item
331: // zero, so we can't support processing of visible data items only
332: state.setProcessVisibleItemsOnly(false);
333: return state;
334: }
335:
336: /**
337: * Returns the number of passes required by the renderer.
338: *
339: * @return 2.
340: */
341: public int getPassCount() {
342: return 2;
343: }
344:
345: /**
346: * Returns the range of values the renderer requires to display all the
347: * items from the specified dataset.
348: *
349: * @param dataset the dataset (<code>null</code> permitted).
350: *
351: * @return The range ([0.0, 0.0] if the dataset contains no values, and
352: * <code>null</code> if the dataset is <code>null</code>).
353: *
354: * @throws ClassCastException if <code>dataset</code> is not an instance
355: * of {@link TableXYDataset}.
356: */
357: public Range findRangeBounds(XYDataset dataset) {
358: if (dataset != null) {
359: return DatasetUtilities
360: .findStackedRangeBounds((TableXYDataset) dataset);
361: } else {
362: return null;
363: }
364: }
365:
366: /**
367: * Draws the visual representation of a single data item.
368: *
369: * @param g2 the graphics device.
370: * @param state the renderer state.
371: * @param dataArea the area within which the data is being drawn.
372: * @param info collects information about the drawing.
373: * @param plot the plot (can be used to obtain standard color information
374: * etc).
375: * @param domainAxis the domain axis.
376: * @param rangeAxis the range axis.
377: * @param dataset the dataset.
378: * @param series the series index (zero-based).
379: * @param item the item index (zero-based).
380: * @param crosshairState information about crosshairs on a plot.
381: * @param pass the pass index.
382: *
383: * @throws ClassCastException if <code>state</code> is not an instance of
384: * <code>StackedXYAreaRendererState</code> or <code>dataset</code>
385: * is not an instance of {@link TableXYDataset}.
386: */
387: public void drawItem(Graphics2D g2, XYItemRendererState state,
388: Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
389: ValueAxis domainAxis, ValueAxis rangeAxis,
390: XYDataset dataset, int series, int item,
391: CrosshairState crosshairState, int pass) {
392:
393: PlotOrientation orientation = plot.getOrientation();
394: StackedXYAreaRendererState areaState = (StackedXYAreaRendererState) state;
395: // Get the item count for the series, so that we can know which is the
396: // end of the series.
397: TableXYDataset tdataset = (TableXYDataset) dataset;
398: int itemCount = tdataset.getItemCount();
399:
400: // get the data point...
401: double x1 = dataset.getXValue(series, item);
402: double y1 = dataset.getYValue(series, item);
403: boolean nullPoint = false;
404: if (Double.isNaN(y1)) {
405: y1 = 0.0;
406: nullPoint = true;
407: }
408:
409: // Get height adjustment based on stack and translate to Java2D values
410: double ph1 = getPreviousHeight(tdataset, series, item);
411: double transX1 = domainAxis.valueToJava2D(x1, dataArea, plot
412: .getDomainAxisEdge());
413: double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea,
414: plot.getRangeAxisEdge());
415:
416: // Get series Paint and Stroke
417: Paint seriesPaint = getItemPaint(series, item);
418: Stroke seriesStroke = getItemStroke(series, item);
419:
420: if (pass == 0) {
421: // On first pass render the areas, line and outlines
422:
423: if (item == 0) {
424: // Create a new Area for the series
425: areaState.setSeriesArea(new Polygon());
426: areaState.setLastSeriesPoints(areaState
427: .getCurrentSeriesPoints());
428: areaState.setCurrentSeriesPoints(new Stack());
429:
430: // start from previous height (ph1)
431: double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
432: plot.getRangeAxisEdge());
433:
434: // The first point is (x, 0)
435: if (orientation == PlotOrientation.VERTICAL) {
436: areaState.getSeriesArea().addPoint((int) transX1,
437: (int) transY2);
438: } else if (orientation == PlotOrientation.HORIZONTAL) {
439: areaState.getSeriesArea().addPoint((int) transY2,
440: (int) transX1);
441: }
442: }
443:
444: // Add each point to Area (x, y)
445: if (orientation == PlotOrientation.VERTICAL) {
446: Point point = new Point((int) transX1, (int) transY1);
447: areaState.getSeriesArea().addPoint((int) point.getX(),
448: (int) point.getY());
449: areaState.getCurrentSeriesPoints().push(point);
450: } else if (orientation == PlotOrientation.HORIZONTAL) {
451: areaState.getSeriesArea().addPoint((int) transY1,
452: (int) transX1);
453: }
454:
455: if (getPlotLines()) {
456: if (item > 0) {
457: // get the previous data point...
458: double x0 = dataset.getXValue(series, item - 1);
459: double y0 = dataset.getYValue(series, item - 1);
460: double ph0 = getPreviousHeight(tdataset, series,
461: item - 1);
462: double transX0 = domainAxis.valueToJava2D(x0,
463: dataArea, plot.getDomainAxisEdge());
464: double transY0 = rangeAxis.valueToJava2D(y0 + ph0,
465: dataArea, plot.getRangeAxisEdge());
466:
467: if (orientation == PlotOrientation.VERTICAL) {
468: areaState.getLine().setLine(transX0, transY0,
469: transX1, transY1);
470: } else if (orientation == PlotOrientation.HORIZONTAL) {
471: areaState.getLine().setLine(transY0, transX0,
472: transY1, transX1);
473: }
474: g2.draw(areaState.getLine());
475: }
476: }
477:
478: // Check if the item is the last item for the series and number of
479: // items > 0. We can't draw an area for a single point.
480: if (getPlotArea() && item > 0 && item == (itemCount - 1)) {
481:
482: double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
483: plot.getRangeAxisEdge());
484:
485: if (orientation == PlotOrientation.VERTICAL) {
486: // Add the last point (x,0)
487: areaState.getSeriesArea().addPoint((int) transX1,
488: (int) transY2);
489: } else if (orientation == PlotOrientation.HORIZONTAL) {
490: // Add the last point (x,0)
491: areaState.getSeriesArea().addPoint((int) transY2,
492: (int) transX1);
493: }
494:
495: // Add points from last series to complete the base of the
496: // polygon
497: if (series != 0) {
498: Stack points = areaState.getLastSeriesPoints();
499: while (!points.empty()) {
500: Point point = (Point) points.pop();
501: areaState.getSeriesArea().addPoint(
502: (int) point.getX(), (int) point.getY());
503: }
504: }
505:
506: // Fill the polygon
507: g2.setPaint(seriesPaint);
508: g2.setStroke(seriesStroke);
509: g2.fill(areaState.getSeriesArea());
510:
511: // Draw an outline around the Area.
512: if (isOutline()) {
513: g2.setStroke(lookupSeriesOutlineStroke(series));
514: g2.setPaint(lookupSeriesOutlinePaint(series));
515: g2.draw(areaState.getSeriesArea());
516: }
517: }
518:
519: int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
520: int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
521: updateCrosshairValues(crosshairState, x1, ph1 + y1,
522: domainAxisIndex, rangeAxisIndex, transX1, transY1,
523: orientation);
524:
525: } else if (pass == 1) {
526: // On second pass render shapes and collect entity and tooltip
527: // information
528:
529: Shape shape = null;
530: if (getPlotShapes()) {
531: shape = getItemShape(series, item);
532: if (plot.getOrientation() == PlotOrientation.VERTICAL) {
533: shape = ShapeUtilities.createTranslatedShape(shape,
534: transX1, transY1);
535: } else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
536: shape = ShapeUtilities.createTranslatedShape(shape,
537: transY1, transX1);
538: }
539: if (!nullPoint) {
540: if (getShapePaint() != null) {
541: g2.setPaint(getShapePaint());
542: } else {
543: g2.setPaint(seriesPaint);
544: }
545: if (getShapeStroke() != null) {
546: g2.setStroke(getShapeStroke());
547: } else {
548: g2.setStroke(seriesStroke);
549: }
550: g2.draw(shape);
551: }
552: } else {
553: if (plot.getOrientation() == PlotOrientation.VERTICAL) {
554: shape = new Rectangle2D.Double(transX1 - 3,
555: transY1 - 3, 6.0, 6.0);
556: } else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
557: shape = new Rectangle2D.Double(transY1 - 3,
558: transX1 - 3, 6.0, 6.0);
559: }
560: }
561:
562: // collect entity and tool tip information...
563: if (state.getInfo() != null) {
564: EntityCollection entities = state.getEntityCollection();
565: if (entities != null && shape != null && !nullPoint) {
566: String tip = null;
567: XYToolTipGenerator generator = getToolTipGenerator(
568: series, item);
569: if (generator != null) {
570: tip = generator.generateToolTip(dataset,
571: series, item);
572: }
573: String url = null;
574: if (getURLGenerator() != null) {
575: url = getURLGenerator().generateURL(dataset,
576: series, item);
577: }
578: XYItemEntity entity = new XYItemEntity(shape,
579: dataset, series, item, tip, url);
580: entities.add(entity);
581: }
582: }
583:
584: }
585: }
586:
587: /**
588: * Calculates the stacked value of the all series up to, but not including
589: * <code>series</code> for the specified item. It returns 0.0 if
590: * <code>series</code> is the first series, i.e. 0.
591: *
592: * @param dataset the dataset.
593: * @param series the series.
594: * @param index the index.
595: *
596: * @return The cumulative value for all series' values up to but excluding
597: * <code>series</code> for <code>index</code>.
598: */
599: protected double getPreviousHeight(TableXYDataset dataset,
600: int series, int index) {
601: double result = 0.0;
602: for (int i = 0; i < series; i++) {
603: double value = dataset.getYValue(i, index);
604: if (!Double.isNaN(value)) {
605: result += value;
606: }
607: }
608: return result;
609: }
610:
611: /**
612: * Tests the renderer for equality with an arbitrary object.
613: *
614: * @param obj the object (<code>null</code> permitted).
615: *
616: * @return A boolean.
617: */
618: public boolean equals(Object obj) {
619: if (obj == this ) {
620: return true;
621: }
622: if (!(obj instanceof StackedXYAreaRenderer)
623: || !super .equals(obj)) {
624: return false;
625: }
626: StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj;
627: if (!PaintUtilities.equal(this .shapePaint, that.shapePaint)) {
628: return false;
629: }
630: if (!ObjectUtilities.equal(this .shapeStroke, that.shapeStroke)) {
631: return false;
632: }
633: return true;
634: }
635:
636: /**
637: * Returns a clone of the renderer.
638: *
639: * @return A clone.
640: *
641: * @throws CloneNotSupportedException if the renderer cannot be cloned.
642: */
643: public Object clone() throws CloneNotSupportedException {
644: return super .clone();
645: }
646:
647: /**
648: * Provides serialization support.
649: *
650: * @param stream the input stream.
651: *
652: * @throws IOException if there is an I/O error.
653: * @throws ClassNotFoundException if there is a classpath problem.
654: */
655: private void readObject(ObjectInputStream stream)
656: throws IOException, ClassNotFoundException {
657: stream.defaultReadObject();
658: this .shapePaint = SerialUtilities.readPaint(stream);
659: this .shapeStroke = SerialUtilities.readStroke(stream);
660: }
661:
662: /**
663: * Provides serialization support.
664: *
665: * @param stream the output stream.
666: *
667: * @throws IOException if there is an I/O error.
668: */
669: private void writeObject(ObjectOutputStream stream)
670: throws IOException {
671: stream.defaultWriteObject();
672: SerialUtilities.writePaint(this.shapePaint, stream);
673: SerialUtilities.writeStroke(this.shapeStroke, stream);
674: }
675:
676: }
|