Frames | No Frames |
1: /* =========================================================== 2: * JFreeChart : a free chart library for the Java(tm) platform 3: * =========================================================== 4: * 5: * (C) Copyright 2000-2008, 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-2008, 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: * Changes: 37: * -------- 38: * 27-Jul-2003 : Initial version (RA); 39: * 30-Jul-2003 : Modified entity constructor (CZ); 40: * 18-Aug-2003 : Now handles null values (RA); 41: * 20-Aug-2003 : Implemented Cloneable, PublicCloneable and Serializable (DG); 42: * 22-Sep-2003 : Changed to be a two pass renderer with optional shape Paint 43: * and Stroke (RA); 44: * 07-Oct-2003 : Added renderer state (DG); 45: * 10-Feb-2004 : Updated state object and changed drawItem() method to make 46: * overriding easier (DG); 47: * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed 48: * XYToolTipGenerator --> XYItemLabelGenerator (DG); 49: * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 50: * getYValue() (DG); 51: * 10-Sep-2004 : Removed getRangeType() method (DG); 52: * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 53: * 06-Jan-2005 : Override equals() (DG); 54: * 07-Jan-2005 : Update for method name changes in DatasetUtilities (DG); 55: * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG); 56: * 06-Jun-2005 : Fixed null pointer exception, plus problems with equals() and 57: * serialization (DG); 58: * ------------- JFREECHART 1.0.x --------------------------------------------- 59: * 10-Nov-2006 : Fixed bug 1593156, NullPointerException with line 60: * plotting (DG); 61: * 02-Feb-2007 : Fixed bug 1649686, crosshairs don't stack y-values (DG); 62: * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 63: * 22-Mar-2007 : Fire change events in setShapePaint() and setShapeStroke() 64: * methods (DG); 65: * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG); 66: * 67: */ 68: 69: package org.jfree.chart.renderer.xy; 70: 71: import java.awt.Graphics2D; 72: import java.awt.Paint; 73: import java.awt.Point; 74: import java.awt.Polygon; 75: import java.awt.Shape; 76: import java.awt.Stroke; 77: import java.awt.geom.Line2D; 78: import java.awt.geom.Rectangle2D; 79: import java.io.IOException; 80: import java.io.ObjectInputStream; 81: import java.io.ObjectOutputStream; 82: import java.io.Serializable; 83: import java.util.Stack; 84: 85: import org.jfree.chart.axis.ValueAxis; 86: import org.jfree.chart.entity.EntityCollection; 87: import org.jfree.chart.entity.XYItemEntity; 88: import org.jfree.chart.event.RendererChangeEvent; 89: import org.jfree.chart.labels.XYToolTipGenerator; 90: import org.jfree.chart.plot.CrosshairState; 91: import org.jfree.chart.plot.PlotOrientation; 92: import org.jfree.chart.plot.PlotRenderingInfo; 93: import org.jfree.chart.plot.XYPlot; 94: import org.jfree.chart.urls.XYURLGenerator; 95: import org.jfree.data.Range; 96: import org.jfree.data.general.DatasetUtilities; 97: import org.jfree.data.xy.TableXYDataset; 98: import org.jfree.data.xy.XYDataset; 99: import org.jfree.io.SerialUtilities; 100: import org.jfree.util.ObjectUtilities; 101: import org.jfree.util.PaintUtilities; 102: import org.jfree.util.PublicCloneable; 103: import org.jfree.util.ShapeUtilities; 104: 105: /** 106: * A stacked area renderer for the {@link XYPlot} class. 107: * <br><br> 108: * SPECIAL NOTE: This renderer does not currently handle negative data values 109: * correctly. This should get fixed at some point, but the current workaround 110: * is to use the {@link StackedXYAreaRenderer2} class instead. 111: */ 112: public class StackedXYAreaRenderer extends XYAreaRenderer 113: implements Cloneable, PublicCloneable, Serializable { 114: 115: /** For serialization. */ 116: private static final long serialVersionUID = 5217394318178570889L; 117: 118: /** 119: * A state object for use by this renderer. 120: */ 121: static class StackedXYAreaRendererState extends XYItemRendererState { 122: 123: /** The area for the current series. */ 124: private Polygon seriesArea; 125: 126: /** The line. */ 127: private Line2D line; 128: 129: /** The points from the last series. */ 130: private Stack lastSeriesPoints; 131: 132: /** The points for the current series. */ 133: private Stack currentSeriesPoints; 134: 135: /** 136: * Creates a new state for the renderer. 137: * 138: * @param info the plot rendering info. 139: */ 140: public StackedXYAreaRendererState(PlotRenderingInfo info) { 141: super(info); 142: this.seriesArea = null; 143: this.line = new Line2D.Double(); 144: this.lastSeriesPoints = new Stack(); 145: this.currentSeriesPoints = new Stack(); 146: } 147: 148: /** 149: * Returns the series area. 150: * 151: * @return The series area. 152: */ 153: public Polygon getSeriesArea() { 154: return this.seriesArea; 155: } 156: 157: /** 158: * Sets the series area. 159: * 160: * @param area the area. 161: */ 162: public void setSeriesArea(Polygon area) { 163: this.seriesArea = area; 164: } 165: 166: /** 167: * Returns the working line. 168: * 169: * @return The working line. 170: */ 171: public Line2D getLine() { 172: return this.line; 173: } 174: 175: /** 176: * Returns the current series points. 177: * 178: * @return The current series points. 179: */ 180: public Stack getCurrentSeriesPoints() { 181: return this.currentSeriesPoints; 182: } 183: 184: /** 185: * Sets the current series points. 186: * 187: * @param points the points. 188: */ 189: public void setCurrentSeriesPoints(Stack points) { 190: this.currentSeriesPoints = points; 191: } 192: 193: /** 194: * Returns the last series points. 195: * 196: * @return The last series points. 197: */ 198: public Stack getLastSeriesPoints() { 199: return this.lastSeriesPoints; 200: } 201: 202: /** 203: * Sets the last series points. 204: * 205: * @param points the points. 206: */ 207: public void setLastSeriesPoints(Stack points) { 208: this.lastSeriesPoints = points; 209: } 210: 211: } 212: 213: /** 214: * Custom Paint for drawing all shapes, if null defaults to series shapes 215: */ 216: private transient Paint shapePaint = null; 217: 218: /** 219: * Custom Stroke for drawing all shapes, if null defaults to series 220: * strokes. 221: */ 222: private transient Stroke shapeStroke = null; 223: 224: /** 225: * Creates a new renderer. 226: */ 227: public StackedXYAreaRenderer() { 228: this(AREA); 229: } 230: 231: /** 232: * Constructs a new renderer. 233: * 234: * @param type the type of the renderer. 235: */ 236: public StackedXYAreaRenderer(int type) { 237: this(type, null, null); 238: } 239: 240: /** 241: * Constructs a new renderer. To specify the type of renderer, use one of 242: * the constants: <code>SHAPES</code>, <code>LINES</code>, 243: * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or 244: * <code>AREA_AND_SHAPES</code>. 245: * 246: * @param type the type of renderer. 247: * @param labelGenerator the tool tip generator to use (<code>null</code> 248: * is none). 249: * @param urlGenerator the URL generator (<code>null</code> permitted). 250: */ 251: public StackedXYAreaRenderer(int type, 252: XYToolTipGenerator labelGenerator, 253: XYURLGenerator urlGenerator) { 254: 255: super(type, labelGenerator, urlGenerator); 256: } 257: 258: /** 259: * Returns the paint used for rendering shapes, or <code>null</code> if 260: * using series paints. 261: * 262: * @return The paint (possibly <code>null</code>). 263: * 264: * @see #setShapePaint(Paint) 265: */ 266: public Paint getShapePaint() { 267: return this.shapePaint; 268: } 269: 270: /** 271: * Sets the paint for rendering shapes and sends a 272: * {@link RendererChangeEvent} to all registered listeners. 273: * 274: * @param shapePaint the paint (<code>null</code> permitted). 275: * 276: * @see #getShapePaint() 277: */ 278: public void setShapePaint(Paint shapePaint) { 279: this.shapePaint = shapePaint; 280: fireChangeEvent(); 281: } 282: 283: /** 284: * Returns the stroke used for rendering shapes, or <code>null</code> if 285: * using series strokes. 286: * 287: * @return The stroke (possibly <code>null</code>). 288: * 289: * @see #setShapeStroke(Stroke) 290: */ 291: public Stroke getShapeStroke() { 292: return this.shapeStroke; 293: } 294: 295: /** 296: * Sets the stroke for rendering shapes and sends a 297: * {@link RendererChangeEvent} to all registered listeners. 298: * 299: * @param shapeStroke the stroke (<code>null</code> permitted). 300: * 301: * @see #getShapeStroke() 302: */ 303: public void setShapeStroke(Stroke shapeStroke) { 304: this.shapeStroke = shapeStroke; 305: fireChangeEvent(); 306: } 307: 308: /** 309: * Initialises the renderer. This method will be called before the first 310: * item is rendered, giving the renderer an opportunity to initialise any 311: * state information it wants to maintain. 312: * 313: * @param g2 the graphics device. 314: * @param dataArea the area inside the axes. 315: * @param plot the plot. 316: * @param data the data. 317: * @param info an optional info collection object to return data back to 318: * the caller. 319: * 320: * @return A state object that should be passed to subsequent calls to the 321: * drawItem() method. 322: */ 323: public XYItemRendererState initialise(Graphics2D g2, 324: Rectangle2D dataArea, 325: XYPlot plot, 326: 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.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(lookupSeriesOutlineStroke(series)); 526: g2.setPaint(lookupSeriesOutlinePaint(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: }