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: * 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, 114: PublicCloneable, 115: 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, 327: XYPlot plot, 328: XYDataset data, 329: PlotRenderingInfo info) { 330: 331: XYItemRendererState state = new StackedXYAreaRendererState(info); 332: // in the rendering process, there is special handling for item 333: // zero, so we can't support processing of visible data items only 334: state.setProcessVisibleItemsOnly(false); 335: return state; 336: } 337: 338: /** 339: * Returns the number of passes required by the renderer. 340: * 341: * @return 2. 342: */ 343: public int getPassCount() { 344: return 2; 345: } 346: 347: /** 348: * Returns the range of values the renderer requires to display all the 349: * items from the specified dataset. 350: * 351: * @param dataset the dataset (<code>null</code> permitted). 352: * 353: * @return The range ([0.0, 0.0] if the dataset contains no values, and 354: * <code>null</code> if the dataset is <code>null</code>). 355: * 356: * @throws ClassCastException if <code>dataset</code> is not an instance 357: * of {@link TableXYDataset}. 358: */ 359: public Range findRangeBounds(XYDataset dataset) { 360: if (dataset != null) { 361: return DatasetUtilities.findStackedRangeBounds( 362: (TableXYDataset) dataset); 363: } 364: else { 365: return null; 366: } 367: } 368: 369: /** 370: * Draws the visual representation of a single data item. 371: * 372: * @param g2 the graphics device. 373: * @param state the renderer state. 374: * @param dataArea the area within which the data is being drawn. 375: * @param info collects information about the drawing. 376: * @param plot the plot (can be used to obtain standard color information 377: * etc). 378: * @param domainAxis the domain axis. 379: * @param rangeAxis the range axis. 380: * @param dataset the dataset. 381: * @param series the series index (zero-based). 382: * @param item the item index (zero-based). 383: * @param crosshairState information about crosshairs on a plot. 384: * @param pass the pass index. 385: * 386: * @throws ClassCastException if <code>state</code> is not an instance of 387: * <code>StackedXYAreaRendererState</code> or <code>dataset</code> 388: * is not an instance of {@link TableXYDataset}. 389: */ 390: public void drawItem(Graphics2D g2, 391: XYItemRendererState state, 392: Rectangle2D dataArea, 393: PlotRenderingInfo info, 394: XYPlot plot, 395: ValueAxis domainAxis, 396: ValueAxis rangeAxis, 397: XYDataset dataset, 398: int series, 399: int item, 400: CrosshairState crosshairState, 401: int pass) { 402: 403: PlotOrientation orientation = plot.getOrientation(); 404: StackedXYAreaRendererState areaState 405: = (StackedXYAreaRendererState) state; 406: // Get the item count for the series, so that we can know which is the 407: // end of the series. 408: TableXYDataset tdataset = (TableXYDataset) dataset; 409: int itemCount = tdataset.getItemCount(); 410: 411: // get the data point... 412: double x1 = dataset.getXValue(series, item); 413: double y1 = dataset.getYValue(series, item); 414: boolean nullPoint = false; 415: if (Double.isNaN(y1)) { 416: y1 = 0.0; 417: nullPoint = true; 418: } 419: 420: // Get height adjustment based on stack and translate to Java2D values 421: double ph1 = getPreviousHeight(tdataset, series, item); 422: double transX1 = domainAxis.valueToJava2D(x1, dataArea, 423: plot.getDomainAxisEdge()); 424: double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea, 425: plot.getRangeAxisEdge()); 426: 427: // Get series Paint and Stroke 428: Paint seriesPaint = getItemPaint(series, item); 429: Stroke seriesStroke = getItemStroke(series, item); 430: 431: if (pass == 0) { 432: // On first pass render the areas, line and outlines 433: 434: if (item == 0) { 435: // Create a new Area for the series 436: areaState.setSeriesArea(new Polygon()); 437: areaState.setLastSeriesPoints( 438: areaState.getCurrentSeriesPoints()); 439: areaState.setCurrentSeriesPoints(new Stack()); 440: 441: // start from previous height (ph1) 442: double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 443: plot.getRangeAxisEdge()); 444: 445: // The first point is (x, 0) 446: if (orientation == PlotOrientation.VERTICAL) { 447: areaState.getSeriesArea().addPoint((int) transX1, 448: (int) transY2); 449: } 450: else if (orientation == PlotOrientation.HORIZONTAL) { 451: areaState.getSeriesArea().addPoint((int) transY2, 452: (int) transX1); 453: } 454: } 455: 456: // Add each point to Area (x, y) 457: if (orientation == PlotOrientation.VERTICAL) { 458: Point point = new Point((int) transX1, (int) transY1); 459: areaState.getSeriesArea().addPoint((int) point.getX(), 460: (int) point.getY()); 461: areaState.getCurrentSeriesPoints().push(point); 462: } 463: else if (orientation == PlotOrientation.HORIZONTAL) { 464: areaState.getSeriesArea().addPoint((int) transY1, 465: (int) transX1); 466: } 467: 468: if (getPlotLines()) { 469: if (item > 0) { 470: // get the previous data point... 471: double x0 = dataset.getXValue(series, item - 1); 472: double y0 = dataset.getYValue(series, item - 1); 473: double ph0 = getPreviousHeight(tdataset, series, item - 1); 474: double transX0 = domainAxis.valueToJava2D(x0, dataArea, 475: plot.getDomainAxisEdge()); 476: double transY0 = rangeAxis.valueToJava2D(y0 + ph0, 477: dataArea, plot.getRangeAxisEdge()); 478: 479: if (orientation == PlotOrientation.VERTICAL) { 480: areaState.getLine().setLine(transX0, transY0, transX1, 481: transY1); 482: } 483: else if (orientation == PlotOrientation.HORIZONTAL) { 484: areaState.getLine().setLine(transY0, transX0, transY1, 485: transX1); 486: } 487: g2.draw(areaState.getLine()); 488: } 489: } 490: 491: // Check if the item is the last item for the series and number of 492: // items > 0. We can't draw an area for a single point. 493: if (getPlotArea() && item > 0 && item == (itemCount - 1)) { 494: 495: double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 496: plot.getRangeAxisEdge()); 497: 498: if (orientation == PlotOrientation.VERTICAL) { 499: // Add the last point (x,0) 500: areaState.getSeriesArea().addPoint((int) transX1, 501: (int) transY2); 502: } 503: else if (orientation == PlotOrientation.HORIZONTAL) { 504: // Add the last point (x,0) 505: areaState.getSeriesArea().addPoint((int) transY2, 506: (int) transX1); 507: } 508: 509: // Add points from last series to complete the base of the 510: // polygon 511: if (series != 0) { 512: Stack points = areaState.getLastSeriesPoints(); 513: while (!points.empty()) { 514: Point point = (Point) points.pop(); 515: areaState.getSeriesArea().addPoint((int) point.getX(), 516: (int) point.getY()); 517: } 518: } 519: 520: // Fill the polygon 521: g2.setPaint(seriesPaint); 522: g2.setStroke(seriesStroke); 523: g2.fill(areaState.getSeriesArea()); 524: 525: // Draw an outline around the Area. 526: if (isOutline()) { 527: g2.setStroke(lookupSeriesOutlineStroke(series)); 528: g2.setPaint(lookupSeriesOutlinePaint(series)); 529: g2.draw(areaState.getSeriesArea()); 530: } 531: } 532: 533: int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 534: int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 535: updateCrosshairValues(crosshairState, x1, ph1 + y1, domainAxisIndex, 536: rangeAxisIndex, transX1, transY1, orientation); 537: 538: } 539: else if (pass == 1) { 540: // On second pass render shapes and collect entity and tooltip 541: // information 542: 543: Shape shape = null; 544: if (getPlotShapes()) { 545: shape = getItemShape(series, item); 546: if (plot.getOrientation() == PlotOrientation.VERTICAL) { 547: shape = ShapeUtilities.createTranslatedShape(shape, 548: transX1, transY1); 549: } 550: else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 551: shape = ShapeUtilities.createTranslatedShape(shape, 552: transY1, transX1); 553: } 554: if (!nullPoint) { 555: if (getShapePaint() != null) { 556: g2.setPaint(getShapePaint()); 557: } 558: else { 559: g2.setPaint(seriesPaint); 560: } 561: if (getShapeStroke() != null) { 562: g2.setStroke(getShapeStroke()); 563: } 564: else { 565: g2.setStroke(seriesStroke); 566: } 567: g2.draw(shape); 568: } 569: } 570: else { 571: if (plot.getOrientation() == PlotOrientation.VERTICAL) { 572: shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3, 573: 6.0, 6.0); 574: } 575: else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 576: shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3, 577: 6.0, 6.0); 578: } 579: } 580: 581: // collect entity and tool tip information... 582: if (state.getInfo() != null) { 583: EntityCollection entities = state.getEntityCollection(); 584: if (entities != null && shape != null && !nullPoint) { 585: String tip = null; 586: XYToolTipGenerator generator 587: = getToolTipGenerator(series, item); 588: if (generator != null) { 589: tip = generator.generateToolTip(dataset, series, item); 590: } 591: String url = null; 592: if (getURLGenerator() != null) { 593: url = getURLGenerator().generateURL(dataset, series, 594: item); 595: } 596: XYItemEntity entity = new XYItemEntity(shape, dataset, 597: series, item, tip, url); 598: entities.add(entity); 599: } 600: } 601: 602: } 603: } 604: 605: /** 606: * Calculates the stacked value of the all series up to, but not including 607: * <code>series</code> for the specified item. It returns 0.0 if 608: * <code>series</code> is the first series, i.e. 0. 609: * 610: * @param dataset the dataset. 611: * @param series the series. 612: * @param index the index. 613: * 614: * @return The cumulative value for all series' values up to but excluding 615: * <code>series</code> for <code>index</code>. 616: */ 617: protected double getPreviousHeight(TableXYDataset dataset, 618: int series, int index) { 619: double result = 0.0; 620: for (int i = 0; i < series; i++) { 621: double value = dataset.getYValue(i, index); 622: if (!Double.isNaN(value)) { 623: result += value; 624: } 625: } 626: return result; 627: } 628: 629: /** 630: * Tests the renderer for equality with an arbitrary object. 631: * 632: * @param obj the object (<code>null</code> permitted). 633: * 634: * @return A boolean. 635: */ 636: public boolean equals(Object obj) { 637: if (obj == this) { 638: return true; 639: } 640: if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) { 641: return false; 642: } 643: StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj; 644: if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) { 645: return false; 646: } 647: if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) { 648: return false; 649: } 650: return true; 651: } 652: 653: /** 654: * Returns a clone of the renderer. 655: * 656: * @return A clone. 657: * 658: * @throws CloneNotSupportedException if the renderer cannot be cloned. 659: */ 660: public Object clone() throws CloneNotSupportedException { 661: return super.clone(); 662: } 663: 664: /** 665: * Provides serialization support. 666: * 667: * @param stream the input stream. 668: * 669: * @throws IOException if there is an I/O error. 670: * @throws ClassNotFoundException if there is a classpath problem. 671: */ 672: private void readObject(ObjectInputStream stream) 673: throws IOException, ClassNotFoundException { 674: stream.defaultReadObject(); 675: this.shapePaint = SerialUtilities.readPaint(stream); 676: this.shapeStroke = SerialUtilities.readStroke(stream); 677: } 678: 679: /** 680: * Provides serialization support. 681: * 682: * @param stream the output stream. 683: * 684: * @throws IOException if there is an I/O error. 685: */ 686: private void writeObject(ObjectOutputStream stream) throws IOException { 687: stream.defaultWriteObject(); 688: SerialUtilities.writePaint(this.shapePaint, stream); 689: SerialUtilities.writeStroke(this.shapeStroke, stream); 690: } 691: 692: }