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.5 2006/01/27 13:52:12 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 ([0.0, 0.0] if the dataset contains no values, and 327 * <code>null</code> if the dataset is <code>null</code>). 328 * 329 * @throws ClassCastException if <code>dataset</code> is not an instance 330 * of {@link TableXYDataset}. 331 */ 332 public Range findRangeBounds(XYDataset dataset) { 333 if (dataset != null) { 334 return DatasetUtilities.findStackedRangeBounds( 335 (TableXYDataset) dataset); 336 } 337 else { 338 return null; 339 } 340 } 341 342 /** 343 * Draws the visual representation of a single data item. 344 * 345 * @param g2 the graphics device. 346 * @param state the renderer state. 347 * @param dataArea the area within which the data is being drawn. 348 * @param info collects information about the drawing. 349 * @param plot the plot (can be used to obtain standard color information 350 * etc). 351 * @param domainAxis the domain axis. 352 * @param rangeAxis the range axis. 353 * @param dataset the dataset. 354 * @param series the series index (zero-based). 355 * @param item the item index (zero-based). 356 * @param crosshairState information about crosshairs on a plot. 357 * @param pass the pass index. 358 * 359 * @throws ClassCastException if <code>state</code> is not an instance of 360 * <code>StackedXYAreaRendererState</code> or <code>dataset</code> 361 * is not an instance of {@link TableXYDataset}. 362 */ 363 public void drawItem(Graphics2D g2, 364 XYItemRendererState state, 365 Rectangle2D dataArea, 366 PlotRenderingInfo info, 367 XYPlot plot, 368 ValueAxis domainAxis, 369 ValueAxis rangeAxis, 370 XYDataset dataset, 371 int series, 372 int item, 373 CrosshairState crosshairState, 374 int pass) { 375 376 PlotOrientation orientation = plot.getOrientation(); 377 StackedXYAreaRendererState areaState 378 = (StackedXYAreaRendererState) state; 379 // Get the item count for the series, so that we can know which is the 380 // end of the series. 381 TableXYDataset tdataset = (TableXYDataset) dataset; 382 int itemCount = tdataset.getItemCount(); 383 384 // get the data point... 385 double x1 = dataset.getXValue(series, item); 386 double y1 = dataset.getYValue(series, item); 387 boolean nullPoint = false; 388 if (Double.isNaN(y1)) { 389 y1 = 0.0; 390 nullPoint = true; 391 } 392 393 // Get height adjustment based on stack and translate to Java2D values 394 double ph1 = getPreviousHeight(tdataset, series, item); 395 double transX1 = domainAxis.valueToJava2D(x1, dataArea, 396 plot.getDomainAxisEdge()); 397 double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea, 398 plot.getRangeAxisEdge()); 399 400 // Get series Paint and Stroke 401 Paint seriesPaint = getItemPaint(series, item); 402 Stroke seriesStroke = getItemStroke(series, item); 403 404 if (pass == 0) { 405 // On first pass render the areas, line and outlines 406 407 if (item == 0) { 408 // Create a new Area for the series 409 areaState.setSeriesArea(new Polygon()); 410 areaState.setLastSeriesPoints( 411 areaState.getCurrentSeriesPoints()); 412 areaState.setCurrentSeriesPoints(new Stack()); 413 414 // start from previous height (ph1) 415 double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 416 plot.getRangeAxisEdge()); 417 418 // The first point is (x, 0) 419 if (orientation == PlotOrientation.VERTICAL) { 420 areaState.getSeriesArea().addPoint((int) transX1, 421 (int) transY2); 422 } 423 else if (orientation == PlotOrientation.HORIZONTAL) { 424 areaState.getSeriesArea().addPoint((int) transY2, 425 (int) transX1); 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((int) point.getX(), 433 (int) point.getY()); 434 areaState.getCurrentSeriesPoints().push(point); 435 } 436 else if (orientation == PlotOrientation.HORIZONTAL) { 437 areaState.getSeriesArea().addPoint((int) transY1, 438 (int) transX1); 439 } 440 441 if (getPlotLines()) { 442 if (item > 0) { 443 // get the previous data point... 444 double x0 = dataset.getXValue(series, item - 1); 445 double y0 = dataset.getYValue(series, item - 1); 446 double ph0 = getPreviousHeight(tdataset, series, item - 1); 447 double transX0 = domainAxis.valueToJava2D(x0, dataArea, 448 plot.getDomainAxisEdge()); 449 double transY0 = rangeAxis.valueToJava2D(y0 + ph0, 450 dataArea, plot.getRangeAxisEdge()); 451 452 if (orientation == PlotOrientation.VERTICAL) { 453 areaState.getLine().setLine(transX0, transY0, transX1, 454 transY1); 455 } 456 else if (orientation == PlotOrientation.HORIZONTAL) { 457 areaState.getLine().setLine(transY0, transX0, transY1, 458 transX1); 459 } 460 g2.draw(areaState.getLine()); 461 } 462 } 463 464 // Check if the item is the last item for the series and number of 465 // items > 0. We can't draw an area for a single point. 466 if (getPlotArea() && item > 0 && item == (itemCount - 1)) { 467 468 double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 469 plot.getRangeAxisEdge()); 470 471 if (orientation == PlotOrientation.VERTICAL) { 472 // Add the last point (x,0) 473 areaState.getSeriesArea().addPoint((int) transX1, 474 (int) transY2); 475 } 476 else if (orientation == PlotOrientation.HORIZONTAL) { 477 // Add the last point (x,0) 478 areaState.getSeriesArea().addPoint((int) transY2, 479 (int) transX1); 480 } 481 482 // Add points from last series to complete the base of the 483 // polygon 484 if (series != 0) { 485 Stack points = areaState.getLastSeriesPoints(); 486 while (!points.empty()) { 487 Point point = (Point) points.pop(); 488 areaState.getSeriesArea().addPoint((int) point.getX(), 489 (int) point.getY()); 490 } 491 } 492 493 // Fill the polygon 494 g2.setPaint(seriesPaint); 495 g2.setStroke(seriesStroke); 496 g2.fill(areaState.getSeriesArea()); 497 498 // Draw an outline around the Area. 499 if (isOutline()) { 500 g2.setStroke(getSeriesOutlineStroke(series)); 501 g2.setPaint(getSeriesOutlinePaint(series)); 502 g2.draw(areaState.getSeriesArea()); 503 } 504 } 505 506 updateCrosshairValues(crosshairState, x1, y1, transX1, transY1, 507 orientation); 508 509 } 510 else if (pass == 1) { 511 // On second pass render shapes and collect entity and tooltip 512 // information 513 514 Shape shape = null; 515 if (getPlotShapes()) { 516 shape = getItemShape(series, item); 517 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 518 shape = ShapeUtilities.createTranslatedShape(shape, 519 transX1, transY1); 520 } 521 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 522 shape = ShapeUtilities.createTranslatedShape(shape, 523 transY1, transX1); 524 } 525 if (!nullPoint) { 526 if (getShapePaint() != null) { 527 g2.setPaint(getShapePaint()); 528 } 529 else { 530 g2.setPaint(seriesPaint); 531 } 532 if (getShapeStroke() != null) { 533 g2.setStroke(getShapeStroke()); 534 } 535 else { 536 g2.setStroke(seriesStroke); 537 } 538 g2.draw(shape); 539 } 540 } 541 else { 542 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 543 shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3, 544 6.0, 6.0); 545 } 546 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 547 shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3, 548 6.0, 6.0); 549 } 550 } 551 552 // collect entity and tool tip information... 553 if (state.getInfo() != null) { 554 EntityCollection entities = state.getEntityCollection(); 555 if (entities != null && shape != null && !nullPoint) { 556 String tip = null; 557 XYToolTipGenerator generator 558 = getToolTipGenerator(series, item); 559 if (generator != null) { 560 tip = generator.generateToolTip(dataset, series, item); 561 } 562 String url = null; 563 if (getURLGenerator() != null) { 564 url = getURLGenerator().generateURL(dataset, series, 565 item); 566 } 567 XYItemEntity entity = new XYItemEntity(shape, dataset, 568 series, item, tip, url); 569 entities.add(entity); 570 } 571 } 572 573 } 574 } 575 576 /** 577 * Calculates the stacked value of the all series up to, but not including 578 * <code>series</code> for the specified item. It returns 0.0 if 579 * <code>series</code> is the first series, i.e. 0. 580 * 581 * @param dataset the dataset. 582 * @param series the series. 583 * @param index the index. 584 * 585 * @return The cumulative value for all series' values up to but excluding 586 * <code>series</code> for <code>index</code>. 587 */ 588 protected double getPreviousHeight(TableXYDataset dataset, 589 int series, int index) { 590 double result = 0.0; 591 for (int i = 0; i < series; i++) { 592 double value = dataset.getYValue(i, index); 593 if (!Double.isNaN(value)) { 594 result += value; 595 } 596 } 597 return result; 598 } 599 600 /** 601 * Tests the renderer for equality with an arbitrary object. 602 * 603 * @param obj the object (<code>null</code> permitted). 604 * 605 * @return A boolean. 606 */ 607 public boolean equals(Object obj) { 608 if (obj == this) { 609 return true; 610 } 611 if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) { 612 return false; 613 } 614 StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj; 615 if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) { 616 return false; 617 } 618 if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) { 619 return false; 620 } 621 return true; 622 } 623 624 /** 625 * Returns a clone of the renderer. 626 * 627 * @return A clone. 628 * 629 * @throws CloneNotSupportedException if the renderer cannot be cloned. 630 */ 631 public Object clone() throws CloneNotSupportedException { 632 return super.clone(); 633 } 634 635 /** 636 * Provides serialization support. 637 * 638 * @param stream the input stream. 639 * 640 * @throws IOException if there is an I/O error. 641 * @throws ClassNotFoundException if there is a classpath problem. 642 */ 643 private void readObject(ObjectInputStream stream) 644 throws IOException, ClassNotFoundException { 645 stream.defaultReadObject(); 646 this.shapePaint = SerialUtilities.readPaint(stream); 647 this.shapeStroke = SerialUtilities.readStroke(stream); 648 } 649 650 /** 651 * Provides serialization support. 652 * 653 * @param stream the output stream. 654 * 655 * @throws IOException if there is an I/O error. 656 */ 657 private void writeObject(ObjectOutputStream stream) throws IOException { 658 stream.defaultWriteObject(); 659 SerialUtilities.writePaint(this.shapePaint, stream); 660 SerialUtilities.writeStroke(this.shapeStroke, stream); 661 } 662 663 }