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