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