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 * $Id: StackedXYAreaRenderer.java,v 1.12.2.9 2007/02/06 16:29:11 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 * ------------- JFREECHART 1.0.x --------------------------------------------- 061 * 10-Nov-2006 : Fixed bug 1593156, NullPointerException with line 062 * plotting (DG); 063 * 02-Feb-2007 : Fixed bug 1649686, crosshairs don't stack y-values (DG); 064 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 065 * 066 */ 067 068 package org.jfree.chart.renderer.xy; 069 070 import java.awt.Graphics2D; 071 import java.awt.Paint; 072 import java.awt.Point; 073 import java.awt.Polygon; 074 import java.awt.Shape; 075 import java.awt.Stroke; 076 import java.awt.geom.Line2D; 077 import java.awt.geom.Rectangle2D; 078 import java.io.IOException; 079 import java.io.ObjectInputStream; 080 import java.io.ObjectOutputStream; 081 import java.io.Serializable; 082 import java.util.Stack; 083 084 import org.jfree.chart.axis.ValueAxis; 085 import org.jfree.chart.entity.EntityCollection; 086 import org.jfree.chart.entity.XYItemEntity; 087 import org.jfree.chart.labels.XYToolTipGenerator; 088 import org.jfree.chart.plot.CrosshairState; 089 import org.jfree.chart.plot.PlotOrientation; 090 import org.jfree.chart.plot.PlotRenderingInfo; 091 import org.jfree.chart.plot.XYPlot; 092 import org.jfree.chart.urls.XYURLGenerator; 093 import org.jfree.data.Range; 094 import org.jfree.data.general.DatasetUtilities; 095 import org.jfree.data.xy.TableXYDataset; 096 import org.jfree.data.xy.XYDataset; 097 import org.jfree.io.SerialUtilities; 098 import org.jfree.util.ObjectUtilities; 099 import org.jfree.util.PaintUtilities; 100 import org.jfree.util.PublicCloneable; 101 import org.jfree.util.ShapeUtilities; 102 103 /** 104 * A stacked area renderer for the {@link XYPlot} class. 105 * <br><br> 106 * SPECIAL NOTE: This renderer does not currently handle negative data values 107 * correctly. This should get fixed at some point, but the current workaround 108 * is to use the {@link StackedXYAreaRenderer2} class instead. 109 */ 110 public class StackedXYAreaRenderer extends XYAreaRenderer 111 implements Cloneable, 112 PublicCloneable, 113 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 * Constructs a new renderer. 232 * 233 * @param type the type of the renderer. 234 */ 235 public StackedXYAreaRenderer(int type) { 236 this(type, null, null); 237 } 238 239 /** 240 * Constructs a new renderer. To specify the type of renderer, use one of 241 * the constants: <code>SHAPES</code>, <code>LINES</code>, 242 * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or 243 * <code>AREA_AND_SHAPES</code>. 244 * 245 * @param type the type of renderer. 246 * @param labelGenerator the tool tip generator to use (<code>null</code> 247 * is none). 248 * @param urlGenerator the URL generator (<code>null</code> permitted). 249 */ 250 public StackedXYAreaRenderer(int type, 251 XYToolTipGenerator labelGenerator, 252 XYURLGenerator urlGenerator) { 253 254 super(type, labelGenerator, urlGenerator); 255 } 256 257 /** 258 * Returns the paint used for rendering shapes, or <code>null</code> if 259 * using series paints. 260 * 261 * @return The Paint. 262 */ 263 public Paint getShapePaint() { 264 return this.shapePaint; 265 } 266 267 /** 268 * Returns the stroke used for rendering shapes, or <code>null</code> if 269 * using series strokes. 270 * 271 * @return The stroke. 272 */ 273 public Stroke getShapeStroke() { 274 return this.shapeStroke; 275 } 276 277 /** 278 * Sets the paint for rendering shapes. 279 * 280 * @param shapePaint the paint (<code>null</code> permitted). 281 */ 282 public void setShapePaint(Paint shapePaint) { 283 this.shapePaint = shapePaint; 284 } 285 286 /** 287 * Sets the stroke for rendering shapes. 288 * 289 * @param shapeStroke the stroke (<code>null</code> permitted). 290 */ 291 public void setShapeStroke(Stroke shapeStroke) { 292 this.shapeStroke = shapeStroke; 293 } 294 295 /** 296 * Initialises the renderer. This method will be called before the first 297 * item is rendered, giving the renderer an opportunity to initialise any 298 * state information it wants to maintain. 299 * 300 * @param g2 the graphics device. 301 * @param dataArea the area inside the axes. 302 * @param plot the plot. 303 * @param data the data. 304 * @param info an optional info collection object to return data back to 305 * the caller. 306 * 307 * @return A state object that should be passed to subsequent calls to the 308 * drawItem() method. 309 */ 310 public XYItemRendererState initialise(Graphics2D g2, 311 Rectangle2D dataArea, 312 XYPlot plot, 313 XYDataset data, 314 PlotRenderingInfo info) { 315 316 return new StackedXYAreaRendererState(info); 317 318 } 319 320 /** 321 * Returns the number of passes required by the renderer. 322 * 323 * @return 2. 324 */ 325 public int getPassCount() { 326 return 2; 327 } 328 329 /** 330 * Returns the range of values the renderer requires to display all the 331 * items from the specified dataset. 332 * 333 * @param dataset the dataset (<code>null</code> permitted). 334 * 335 * @return The range ([0.0, 0.0] if the dataset contains no values, and 336 * <code>null</code> if the dataset is <code>null</code>). 337 * 338 * @throws ClassCastException if <code>dataset</code> is not an instance 339 * of {@link TableXYDataset}. 340 */ 341 public Range findRangeBounds(XYDataset dataset) { 342 if (dataset != null) { 343 return DatasetUtilities.findStackedRangeBounds( 344 (TableXYDataset) dataset); 345 } 346 else { 347 return null; 348 } 349 } 350 351 /** 352 * Draws the visual representation of a single data item. 353 * 354 * @param g2 the graphics device. 355 * @param state the renderer state. 356 * @param dataArea the area within which the data is being drawn. 357 * @param info collects information about the drawing. 358 * @param plot the plot (can be used to obtain standard color information 359 * etc). 360 * @param domainAxis the domain axis. 361 * @param rangeAxis the range axis. 362 * @param dataset the dataset. 363 * @param series the series index (zero-based). 364 * @param item the item index (zero-based). 365 * @param crosshairState information about crosshairs on a plot. 366 * @param pass the pass index. 367 * 368 * @throws ClassCastException if <code>state</code> is not an instance of 369 * <code>StackedXYAreaRendererState</code> or <code>dataset</code> 370 * is not an instance of {@link TableXYDataset}. 371 */ 372 public void drawItem(Graphics2D g2, 373 XYItemRendererState state, 374 Rectangle2D dataArea, 375 PlotRenderingInfo info, 376 XYPlot plot, 377 ValueAxis domainAxis, 378 ValueAxis rangeAxis, 379 XYDataset dataset, 380 int series, 381 int item, 382 CrosshairState crosshairState, 383 int pass) { 384 385 PlotOrientation orientation = plot.getOrientation(); 386 StackedXYAreaRendererState areaState 387 = (StackedXYAreaRendererState) state; 388 // Get the item count for the series, so that we can know which is the 389 // end of the series. 390 TableXYDataset tdataset = (TableXYDataset) dataset; 391 int itemCount = tdataset.getItemCount(); 392 393 // get the data point... 394 double x1 = dataset.getXValue(series, item); 395 double y1 = dataset.getYValue(series, item); 396 boolean nullPoint = false; 397 if (Double.isNaN(y1)) { 398 y1 = 0.0; 399 nullPoint = true; 400 } 401 402 // Get height adjustment based on stack and translate to Java2D values 403 double ph1 = getPreviousHeight(tdataset, series, item); 404 double transX1 = domainAxis.valueToJava2D(x1, dataArea, 405 plot.getDomainAxisEdge()); 406 double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea, 407 plot.getRangeAxisEdge()); 408 409 // Get series Paint and Stroke 410 Paint seriesPaint = getItemPaint(series, item); 411 Stroke seriesStroke = getItemStroke(series, item); 412 413 if (pass == 0) { 414 // On first pass render the areas, line and outlines 415 416 if (item == 0) { 417 // Create a new Area for the series 418 areaState.setSeriesArea(new Polygon()); 419 areaState.setLastSeriesPoints( 420 areaState.getCurrentSeriesPoints()); 421 areaState.setCurrentSeriesPoints(new Stack()); 422 423 // start from previous height (ph1) 424 double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 425 plot.getRangeAxisEdge()); 426 427 // The first point is (x, 0) 428 if (orientation == PlotOrientation.VERTICAL) { 429 areaState.getSeriesArea().addPoint((int) transX1, 430 (int) transY2); 431 } 432 else if (orientation == PlotOrientation.HORIZONTAL) { 433 areaState.getSeriesArea().addPoint((int) transY2, 434 (int) transX1); 435 } 436 } 437 438 // Add each point to Area (x, y) 439 if (orientation == PlotOrientation.VERTICAL) { 440 Point point = new Point((int) transX1, (int) transY1); 441 areaState.getSeriesArea().addPoint((int) point.getX(), 442 (int) point.getY()); 443 areaState.getCurrentSeriesPoints().push(point); 444 } 445 else if (orientation == PlotOrientation.HORIZONTAL) { 446 areaState.getSeriesArea().addPoint((int) transY1, 447 (int) transX1); 448 } 449 450 if (getPlotLines()) { 451 if (item > 0) { 452 // get the previous data point... 453 double x0 = dataset.getXValue(series, item - 1); 454 double y0 = dataset.getYValue(series, item - 1); 455 double ph0 = getPreviousHeight(tdataset, series, item - 1); 456 double transX0 = domainAxis.valueToJava2D(x0, dataArea, 457 plot.getDomainAxisEdge()); 458 double transY0 = rangeAxis.valueToJava2D(y0 + ph0, 459 dataArea, plot.getRangeAxisEdge()); 460 461 if (orientation == PlotOrientation.VERTICAL) { 462 areaState.getLine().setLine(transX0, transY0, transX1, 463 transY1); 464 } 465 else if (orientation == PlotOrientation.HORIZONTAL) { 466 areaState.getLine().setLine(transY0, transX0, transY1, 467 transX1); 468 } 469 g2.draw(areaState.getLine()); 470 } 471 } 472 473 // Check if the item is the last item for the series and number of 474 // items > 0. We can't draw an area for a single point. 475 if (getPlotArea() && item > 0 && item == (itemCount - 1)) { 476 477 double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 478 plot.getRangeAxisEdge()); 479 480 if (orientation == PlotOrientation.VERTICAL) { 481 // Add the last point (x,0) 482 areaState.getSeriesArea().addPoint((int) transX1, 483 (int) transY2); 484 } 485 else if (orientation == PlotOrientation.HORIZONTAL) { 486 // Add the last point (x,0) 487 areaState.getSeriesArea().addPoint((int) transY2, 488 (int) transX1); 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((int) point.getX(), 498 (int) point.getY()); 499 } 500 } 501 502 // Fill the polygon 503 g2.setPaint(seriesPaint); 504 g2.setStroke(seriesStroke); 505 g2.fill(areaState.getSeriesArea()); 506 507 // Draw an outline around the Area. 508 if (isOutline()) { 509 g2.setStroke(getSeriesOutlineStroke(series)); 510 g2.setPaint(getSeriesOutlinePaint(series)); 511 g2.draw(areaState.getSeriesArea()); 512 } 513 } 514 515 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 516 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 517 updateCrosshairValues(crosshairState, x1, ph1 + y1, domainAxisIndex, 518 rangeAxisIndex, transX1, transY1, orientation); 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(shape, 530 transX1, transY1); 531 } 532 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 533 shape = ShapeUtilities.createTranslatedShape(shape, 534 transY1, transX1); 535 } 536 if (!nullPoint) { 537 if (getShapePaint() != null) { 538 g2.setPaint(getShapePaint()); 539 } 540 else { 541 g2.setPaint(seriesPaint); 542 } 543 if (getShapeStroke() != null) { 544 g2.setStroke(getShapeStroke()); 545 } 546 else { 547 g2.setStroke(seriesStroke); 548 } 549 g2.draw(shape); 550 } 551 } 552 else { 553 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 554 shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3, 555 6.0, 6.0); 556 } 557 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 558 shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3, 559 6.0, 6.0); 560 } 561 } 562 563 // collect entity and tool tip information... 564 if (state.getInfo() != null) { 565 EntityCollection entities = state.getEntityCollection(); 566 if (entities != null && shape != null && !nullPoint) { 567 String tip = null; 568 XYToolTipGenerator generator 569 = getToolTipGenerator(series, item); 570 if (generator != null) { 571 tip = generator.generateToolTip(dataset, series, item); 572 } 573 String url = null; 574 if (getURLGenerator() != null) { 575 url = getURLGenerator().generateURL(dataset, series, 576 item); 577 } 578 XYItemEntity entity = new XYItemEntity(shape, dataset, 579 series, item, tip, url); 580 entities.add(entity); 581 } 582 } 583 584 } 585 } 586 587 /** 588 * Calculates the stacked value of the all series up to, but not including 589 * <code>series</code> for the specified item. It returns 0.0 if 590 * <code>series</code> is the first series, i.e. 0. 591 * 592 * @param dataset the dataset. 593 * @param series the series. 594 * @param index the index. 595 * 596 * @return The cumulative value for all series' values up to but excluding 597 * <code>series</code> for <code>index</code>. 598 */ 599 protected double getPreviousHeight(TableXYDataset dataset, 600 int series, int index) { 601 double result = 0.0; 602 for (int i = 0; i < series; i++) { 603 double value = dataset.getYValue(i, index); 604 if (!Double.isNaN(value)) { 605 result += value; 606 } 607 } 608 return result; 609 } 610 611 /** 612 * Tests the renderer for equality with an arbitrary object. 613 * 614 * @param obj the object (<code>null</code> permitted). 615 * 616 * @return A boolean. 617 */ 618 public boolean equals(Object obj) { 619 if (obj == this) { 620 return true; 621 } 622 if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) { 623 return false; 624 } 625 StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj; 626 if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) { 627 return false; 628 } 629 if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) { 630 return false; 631 } 632 return true; 633 } 634 635 /** 636 * Returns a clone of the renderer. 637 * 638 * @return A clone. 639 * 640 * @throws CloneNotSupportedException if the renderer cannot be cloned. 641 */ 642 public Object clone() throws CloneNotSupportedException { 643 return super.clone(); 644 } 645 646 /** 647 * Provides serialization support. 648 * 649 * @param stream the input stream. 650 * 651 * @throws IOException if there is an I/O error. 652 * @throws ClassNotFoundException if there is a classpath problem. 653 */ 654 private void readObject(ObjectInputStream stream) 655 throws IOException, ClassNotFoundException { 656 stream.defaultReadObject(); 657 this.shapePaint = SerialUtilities.readPaint(stream); 658 this.shapeStroke = SerialUtilities.readStroke(stream); 659 } 660 661 /** 662 * Provides serialization support. 663 * 664 * @param stream the output stream. 665 * 666 * @throws IOException if there is an I/O error. 667 */ 668 private void writeObject(ObjectOutputStream stream) throws IOException { 669 stream.defaultWriteObject(); 670 SerialUtilities.writePaint(this.shapePaint, stream); 671 SerialUtilities.writeStroke(this.shapeStroke, stream); 672 } 673 674 }