Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 6: * 7: * Project Info: http://www.jfree.org/jfreechart/index.html 8: * 9: * This library is free software; you can redistribute it and/or modify it 10: * under the terms of the GNU Lesser General Public License as published by 11: * the Free Software Foundation; either version 2.1 of the License, or 12: * (at your option) any later version. 13: * 14: * This library is distributed in the hope that it will be useful, but 15: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17: * License for more details. 18: * 19: * You should have received a copy of the GNU Lesser General Public 20: * License along with this library; if not, write to the Free Software 21: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22: * USA. 23: * 24: * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 25: * in the United States and other countries.] 26: * 27: * -------------------------- 28: * StackedXYAreaRenderer.java 29: * -------------------------- 30: * (C) Copyright 2003-2007, by Richard Atkinson and Contributors. 31: * 32: * Original Author: Richard Atkinson; 33: * Contributor(s): Christian W. Zuckschwerdt; 34: * David Gilbert (for Object Refinery Limited); 35: * 36: * $Id: StackedXYAreaRenderer.java,v 1.12.2.11 2007/03/22 17:11:54 mungady Exp $ 37: * 38: * Changes: 39: * -------- 40: * 27-Jul-2003 : Initial version (RA); 41: * 30-Jul-2003 : Modified entity constructor (CZ); 42: * 18-Aug-2003 : Now handles null values (RA); 43: * 20-Aug-2003 : Implemented Cloneable, PublicCloneable and Serializable (DG); 44: * 22-Sep-2003 : Changed to be a two pass renderer with optional shape Paint 45: * and Stroke (RA); 46: * 07-Oct-2003 : Added renderer state (DG); 47: * 10-Feb-2004 : Updated state object and changed drawItem() method to make 48: * overriding easier (DG); 49: * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed 50: * XYToolTipGenerator --> XYItemLabelGenerator (DG); 51: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 52: * getYValue() (DG); 53: * 10-Sep-2004 : Removed getRangeType() method (DG); 54: * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 55: * 06-Jan-2005 : Override equals() (DG); 56: * 07-Jan-2005 : Update for method name changes in DatasetUtilities (DG); 57: * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG); 58: * 06-Jun-2005 : Fixed null pointer exception, plus problems with equals() and 59: * serialization (DG); 60: * ------------- JFREECHART 1.0.x --------------------------------------------- 61: * 10-Nov-2006 : Fixed bug 1593156, NullPointerException with line 62: * plotting (DG); 63: * 02-Feb-2007 : Fixed bug 1649686, crosshairs don't stack y-values (DG); 64: * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 65: * 22-Mar-2007 : Fire change events in setShapePaint() and setShapeStroke() 66: * methods (DG); 67: * 68: */ 69: 70: package org.jfree.chart.renderer.xy; 71: 72: import java.awt.Graphics2D; 73: import java.awt.Paint; 74: import java.awt.Point; 75: import java.awt.Polygon; 76: import java.awt.Shape; 77: import java.awt.Stroke; 78: import java.awt.geom.Line2D; 79: import java.awt.geom.Rectangle2D; 80: import java.io.IOException; 81: import java.io.ObjectInputStream; 82: import java.io.ObjectOutputStream; 83: import java.io.Serializable; 84: import java.util.Stack; 85: 86: import org.jfree.chart.axis.ValueAxis; 87: import org.jfree.chart.entity.EntityCollection; 88: import org.jfree.chart.entity.XYItemEntity; 89: import org.jfree.chart.event.RendererChangeEvent; 90: import org.jfree.chart.labels.XYToolTipGenerator; 91: import org.jfree.chart.plot.CrosshairState; 92: import org.jfree.chart.plot.PlotOrientation; 93: import org.jfree.chart.plot.PlotRenderingInfo; 94: import org.jfree.chart.plot.XYPlot; 95: import org.jfree.chart.urls.XYURLGenerator; 96: import org.jfree.data.Range; 97: import org.jfree.data.general.DatasetUtilities; 98: import org.jfree.data.xy.TableXYDataset; 99: import org.jfree.data.xy.XYDataset; 100: import org.jfree.io.SerialUtilities; 101: import org.jfree.util.ObjectUtilities; 102: import org.jfree.util.PaintUtilities; 103: import org.jfree.util.PublicCloneable; 104: import org.jfree.util.ShapeUtilities; 105: 106: /** 107: * A stacked area renderer for the {@link XYPlot} class. 108: * <br><br> 109: * SPECIAL NOTE: This renderer does not currently handle negative data values 110: * correctly. This should get fixed at some point, but the current workaround 111: * is to use the {@link StackedXYAreaRenderer2} class instead. 112: */ 113: public class StackedXYAreaRenderer extends XYAreaRenderer 114: implements Cloneable, 115: PublicCloneable, 116: Serializable { 117: 118: /** For serialization. */ 119: private static final long serialVersionUID = 5217394318178570889L; 120: 121: /** 122: * A state object for use by this renderer. 123: */ 124: static class StackedXYAreaRendererState extends XYItemRendererState { 125: 126: /** The area for the current series. */ 127: private Polygon seriesArea; 128: 129: /** The line. */ 130: private Line2D line; 131: 132: /** The points from the last series. */ 133: private Stack lastSeriesPoints; 134: 135: /** The points for the current series. */ 136: private Stack currentSeriesPoints; 137: 138: /** 139: * Creates a new state for the renderer. 140: * 141: * @param info the plot rendering info. 142: */ 143: public StackedXYAreaRendererState(PlotRenderingInfo info) { 144: super(info); 145: this.seriesArea = null; 146: this.line = new Line2D.Double(); 147: this.lastSeriesPoints = new Stack(); 148: this.currentSeriesPoints = new Stack(); 149: } 150: 151: /** 152: * Returns the series area. 153: * 154: * @return The series area. 155: */ 156: public Polygon getSeriesArea() { 157: return this.seriesArea; 158: } 159: 160: /** 161: * Sets the series area. 162: * 163: * @param area the area. 164: */ 165: public void setSeriesArea(Polygon area) { 166: this.seriesArea = area; 167: } 168: 169: /** 170: * Returns the working line. 171: * 172: * @return The working line. 173: */ 174: public Line2D getLine() { 175: return this.line; 176: } 177: 178: /** 179: * Returns the current series points. 180: * 181: * @return The current series points. 182: */ 183: public Stack getCurrentSeriesPoints() { 184: return this.currentSeriesPoints; 185: } 186: 187: /** 188: * Sets the current series points. 189: * 190: * @param points the points. 191: */ 192: public void setCurrentSeriesPoints(Stack points) { 193: this.currentSeriesPoints = points; 194: } 195: 196: /** 197: * Returns the last series points. 198: * 199: * @return The last series points. 200: */ 201: public Stack getLastSeriesPoints() { 202: return this.lastSeriesPoints; 203: } 204: 205: /** 206: * Sets the last series points. 207: * 208: * @param points the points. 209: */ 210: public void setLastSeriesPoints(Stack points) { 211: this.lastSeriesPoints = points; 212: } 213: 214: } 215: 216: /** 217: * Custom Paint for drawing all shapes, if null defaults to series shapes 218: */ 219: private transient Paint shapePaint = null; 220: 221: /** 222: * Custom Stroke for drawing all shapes, if null defaults to series 223: * strokes. 224: */ 225: private transient Stroke shapeStroke = null; 226: 227: /** 228: * Creates a new renderer. 229: */ 230: public StackedXYAreaRenderer() { 231: this(AREA); 232: } 233: 234: /** 235: * Constructs a new renderer. 236: * 237: * @param type the type of the renderer. 238: */ 239: public StackedXYAreaRenderer(int type) { 240: this(type, null, null); 241: } 242: 243: /** 244: * Constructs a new renderer. To specify the type of renderer, use one of 245: * the constants: <code>SHAPES</code>, <code>LINES</code>, 246: * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or 247: * <code>AREA_AND_SHAPES</code>. 248: * 249: * @param type the type of renderer. 250: * @param labelGenerator the tool tip generator to use (<code>null</code> 251: * is none). 252: * @param urlGenerator the URL generator (<code>null</code> permitted). 253: */ 254: public StackedXYAreaRenderer(int type, 255: XYToolTipGenerator labelGenerator, 256: XYURLGenerator urlGenerator) { 257: 258: super(type, labelGenerator, urlGenerator); 259: } 260: 261: /** 262: * Returns the paint used for rendering shapes, or <code>null</code> if 263: * using series paints. 264: * 265: * @return The paint (possibly <code>null</code>). 266: * 267: * @see #setShapePaint(Paint) 268: */ 269: public Paint getShapePaint() { 270: return this.shapePaint; 271: } 272: 273: /** 274: * Sets the paint for rendering shapes and sends a 275: * {@link RendererChangeEvent} to all registered listeners. 276: * 277: * @param shapePaint the paint (<code>null</code> permitted). 278: * 279: * @see #getShapePaint() 280: */ 281: public void setShapePaint(Paint shapePaint) { 282: this.shapePaint = shapePaint; 283: fireChangeEvent(); 284: } 285: 286: /** 287: * Returns the stroke used for rendering shapes, or <code>null</code> if 288: * using series strokes. 289: * 290: * @return The stroke (possibly <code>null</code>). 291: * 292: * @see #setShapeStroke(Stroke) 293: */ 294: public Stroke getShapeStroke() { 295: return this.shapeStroke; 296: } 297: 298: /** 299: * Sets the stroke for rendering shapes and sends a 300: * {@link RendererChangeEvent} to all registered listeners. 301: * 302: * @param shapeStroke the stroke (<code>null</code> permitted). 303: * 304: * @see #getShapeStroke() 305: */ 306: public void setShapeStroke(Stroke shapeStroke) { 307: this.shapeStroke = shapeStroke; 308: fireChangeEvent(); 309: } 310: 311: /** 312: * Initialises the renderer. This method will be called before the first 313: * item is rendered, giving the renderer an opportunity to initialise any 314: * state information it wants to maintain. 315: * 316: * @param g2 the graphics device. 317: * @param dataArea the area inside the axes. 318: * @param plot the plot. 319: * @param data the data. 320: * @param info an optional info collection object to return data back to 321: * the caller. 322: * 323: * @return A state object that should be passed to subsequent calls to the 324: * drawItem() method. 325: */ 326: public XYItemRendererState initialise(Graphics2D g2, 327: Rectangle2D dataArea, 328: XYPlot plot, 329: XYDataset data, 330: PlotRenderingInfo info) { 331: 332: return new StackedXYAreaRendererState(info); 333: 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.findStackedRangeBounds( 360: (TableXYDataset) dataset); 361: } 362: else { 363: return null; 364: } 365: } 366: 367: /** 368: * Draws the visual representation of a single data item. 369: * 370: * @param g2 the graphics device. 371: * @param state the renderer state. 372: * @param dataArea the area within which the data is being drawn. 373: * @param info collects information about the drawing. 374: * @param plot the plot (can be used to obtain standard color information 375: * etc). 376: * @param domainAxis the domain axis. 377: * @param rangeAxis the range axis. 378: * @param dataset the dataset. 379: * @param series the series index (zero-based). 380: * @param item the item index (zero-based). 381: * @param crosshairState information about crosshairs on a plot. 382: * @param pass the pass index. 383: * 384: * @throws ClassCastException if <code>state</code> is not an instance of 385: * <code>StackedXYAreaRendererState</code> or <code>dataset</code> 386: * is not an instance of {@link TableXYDataset}. 387: */ 388: public void drawItem(Graphics2D g2, 389: XYItemRendererState state, 390: Rectangle2D dataArea, 391: PlotRenderingInfo info, 392: XYPlot plot, 393: ValueAxis domainAxis, 394: ValueAxis rangeAxis, 395: XYDataset dataset, 396: int series, 397: int item, 398: CrosshairState crosshairState, 399: int pass) { 400: 401: PlotOrientation orientation = plot.getOrientation(); 402: StackedXYAreaRendererState areaState 403: = (StackedXYAreaRendererState) state; 404: // Get the item count for the series, so that we can know which is the 405: // end of the series. 406: TableXYDataset tdataset = (TableXYDataset) dataset; 407: int itemCount = tdataset.getItemCount(); 408: 409: // get the data point... 410: double x1 = dataset.getXValue(series, item); 411: double y1 = dataset.getYValue(series, item); 412: boolean nullPoint = false; 413: if (Double.isNaN(y1)) { 414: y1 = 0.0; 415: nullPoint = true; 416: } 417: 418: // Get height adjustment based on stack and translate to Java2D values 419: double ph1 = getPreviousHeight(tdataset, series, item); 420: double transX1 = domainAxis.valueToJava2D(x1, dataArea, 421: plot.getDomainAxisEdge()); 422: double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea, 423: plot.getRangeAxisEdge()); 424: 425: // Get series Paint and Stroke 426: Paint seriesPaint = getItemPaint(series, item); 427: Stroke seriesStroke = getItemStroke(series, item); 428: 429: if (pass == 0) { 430: // On first pass render the areas, line and outlines 431: 432: if (item == 0) { 433: // Create a new Area for the series 434: areaState.setSeriesArea(new Polygon()); 435: areaState.setLastSeriesPoints( 436: areaState.getCurrentSeriesPoints()); 437: areaState.setCurrentSeriesPoints(new Stack()); 438: 439: // start from previous height (ph1) 440: double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 441: plot.getRangeAxisEdge()); 442: 443: // The first point is (x, 0) 444: if (orientation == PlotOrientation.VERTICAL) { 445: areaState.getSeriesArea().addPoint((int) transX1, 446: (int) transY2); 447: } 448: else if (orientation == PlotOrientation.HORIZONTAL) { 449: areaState.getSeriesArea().addPoint((int) transY2, 450: (int) transX1); 451: } 452: } 453: 454: // Add each point to Area (x, y) 455: if (orientation == PlotOrientation.VERTICAL) { 456: Point point = new Point((int) transX1, (int) transY1); 457: areaState.getSeriesArea().addPoint((int) point.getX(), 458: (int) point.getY()); 459: areaState.getCurrentSeriesPoints().push(point); 460: } 461: else if (orientation == PlotOrientation.HORIZONTAL) { 462: areaState.getSeriesArea().addPoint((int) transY1, 463: (int) transX1); 464: } 465: 466: if (getPlotLines()) { 467: if (item > 0) { 468: // get the previous data point... 469: double x0 = dataset.getXValue(series, item - 1); 470: double y0 = dataset.getYValue(series, item - 1); 471: double ph0 = getPreviousHeight(tdataset, series, item - 1); 472: double transX0 = domainAxis.valueToJava2D(x0, dataArea, 473: plot.getDomainAxisEdge()); 474: double transY0 = rangeAxis.valueToJava2D(y0 + ph0, 475: dataArea, plot.getRangeAxisEdge()); 476: 477: if (orientation == PlotOrientation.VERTICAL) { 478: areaState.getLine().setLine(transX0, transY0, transX1, 479: transY1); 480: } 481: else if (orientation == PlotOrientation.HORIZONTAL) { 482: areaState.getLine().setLine(transY0, transX0, transY1, 483: transX1); 484: } 485: g2.draw(areaState.getLine()); 486: } 487: } 488: 489: // Check if the item is the last item for the series and number of 490: // items > 0. We can't draw an area for a single point. 491: if (getPlotArea() && item > 0 && item == (itemCount - 1)) { 492: 493: double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 494: plot.getRangeAxisEdge()); 495: 496: if (orientation == PlotOrientation.VERTICAL) { 497: // Add the last point (x,0) 498: areaState.getSeriesArea().addPoint((int) transX1, 499: (int) transY2); 500: } 501: else if (orientation == PlotOrientation.HORIZONTAL) { 502: // Add the last point (x,0) 503: areaState.getSeriesArea().addPoint((int) transY2, 504: (int) transX1); 505: } 506: 507: // Add points from last series to complete the base of the 508: // polygon 509: if (series != 0) { 510: Stack points = areaState.getLastSeriesPoints(); 511: while (!points.empty()) { 512: Point point = (Point) points.pop(); 513: areaState.getSeriesArea().addPoint((int) point.getX(), 514: (int) point.getY()); 515: } 516: } 517: 518: // Fill the polygon 519: g2.setPaint(seriesPaint); 520: g2.setStroke(seriesStroke); 521: g2.fill(areaState.getSeriesArea()); 522: 523: // Draw an outline around the Area. 524: if (isOutline()) { 525: g2.setStroke(getSeriesOutlineStroke(series)); 526: g2.setPaint(getSeriesOutlinePaint(series)); 527: g2.draw(areaState.getSeriesArea()); 528: } 529: } 530: 531: int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 532: int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 533: updateCrosshairValues(crosshairState, x1, ph1 + y1, domainAxisIndex, 534: rangeAxisIndex, transX1, transY1, orientation); 535: 536: } 537: else if (pass == 1) { 538: // On second pass render shapes and collect entity and tooltip 539: // information 540: 541: Shape shape = null; 542: if (getPlotShapes()) { 543: shape = getItemShape(series, item); 544: if (plot.getOrientation() == PlotOrientation.VERTICAL) { 545: shape = ShapeUtilities.createTranslatedShape(shape, 546: transX1, transY1); 547: } 548: else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 549: shape = ShapeUtilities.createTranslatedShape(shape, 550: transY1, transX1); 551: } 552: if (!nullPoint) { 553: if (getShapePaint() != null) { 554: g2.setPaint(getShapePaint()); 555: } 556: else { 557: g2.setPaint(seriesPaint); 558: } 559: if (getShapeStroke() != null) { 560: g2.setStroke(getShapeStroke()); 561: } 562: else { 563: g2.setStroke(seriesStroke); 564: } 565: g2.draw(shape); 566: } 567: } 568: else { 569: if (plot.getOrientation() == PlotOrientation.VERTICAL) { 570: shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3, 571: 6.0, 6.0); 572: } 573: else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 574: shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3, 575: 6.0, 6.0); 576: } 577: } 578: 579: // collect entity and tool tip information... 580: if (state.getInfo() != null) { 581: EntityCollection entities = state.getEntityCollection(); 582: if (entities != null && shape != null && !nullPoint) { 583: String tip = null; 584: XYToolTipGenerator generator 585: = getToolTipGenerator(series, item); 586: if (generator != null) { 587: tip = generator.generateToolTip(dataset, series, item); 588: } 589: String url = null; 590: if (getURLGenerator() != null) { 591: url = getURLGenerator().generateURL(dataset, series, 592: item); 593: } 594: XYItemEntity entity = new XYItemEntity(shape, dataset, 595: series, item, tip, url); 596: entities.add(entity); 597: } 598: } 599: 600: } 601: } 602: 603: /** 604: * Calculates the stacked value of the all series up to, but not including 605: * <code>series</code> for the specified item. It returns 0.0 if 606: * <code>series</code> is the first series, i.e. 0. 607: * 608: * @param dataset the dataset. 609: * @param series the series. 610: * @param index the index. 611: * 612: * @return The cumulative value for all series' values up to but excluding 613: * <code>series</code> for <code>index</code>. 614: */ 615: protected double getPreviousHeight(TableXYDataset dataset, 616: int series, int index) { 617: double result = 0.0; 618: for (int i = 0; i < series; i++) { 619: double value = dataset.getYValue(i, index); 620: if (!Double.isNaN(value)) { 621: result += value; 622: } 623: } 624: return result; 625: } 626: 627: /** 628: * Tests the renderer for equality with an arbitrary object. 629: * 630: * @param obj the object (<code>null</code> permitted). 631: * 632: * @return A boolean. 633: */ 634: public boolean equals(Object obj) { 635: if (obj == this) { 636: return true; 637: } 638: if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) { 639: return false; 640: } 641: StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj; 642: if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) { 643: return false; 644: } 645: if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) { 646: return false; 647: } 648: return true; 649: } 650: 651: /** 652: * Returns a clone of the renderer. 653: * 654: * @return A clone. 655: * 656: * @throws CloneNotSupportedException if the renderer cannot be cloned. 657: */ 658: public Object clone() throws CloneNotSupportedException { 659: return super.clone(); 660: } 661: 662: /** 663: * Provides serialization support. 664: * 665: * @param stream the input stream. 666: * 667: * @throws IOException if there is an I/O error. 668: * @throws ClassNotFoundException if there is a classpath problem. 669: */ 670: private void readObject(ObjectInputStream stream) 671: throws IOException, ClassNotFoundException { 672: stream.defaultReadObject(); 673: this.shapePaint = SerialUtilities.readPaint(stream); 674: this.shapeStroke = SerialUtilities.readStroke(stream); 675: } 676: 677: /** 678: * Provides serialization support. 679: * 680: * @param stream the output stream. 681: * 682: * @throws IOException if there is an I/O error. 683: */ 684: private void writeObject(ObjectOutputStream stream) throws IOException { 685: stream.defaultWriteObject(); 686: SerialUtilities.writePaint(this.shapePaint, stream); 687: SerialUtilities.writeStroke(this.shapeStroke, stream); 688: } 689: 690: }