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 * 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 }