001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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-2007, 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, 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 }