001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * -------------------------- 028 * StackedXYAreaRenderer.java 029 * -------------------------- 030 * (C) Copyright 2003-2014, 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 069package org.jfree.chart.renderer.xy; 070 071import java.awt.Graphics2D; 072import java.awt.Paint; 073import java.awt.Point; 074import java.awt.Polygon; 075import java.awt.Shape; 076import java.awt.Stroke; 077import java.awt.geom.Line2D; 078import java.awt.geom.Rectangle2D; 079import java.io.IOException; 080import java.io.ObjectInputStream; 081import java.io.ObjectOutputStream; 082import java.io.Serializable; 083import java.util.Stack; 084 085import org.jfree.chart.axis.ValueAxis; 086import org.jfree.chart.entity.EntityCollection; 087import org.jfree.chart.entity.XYItemEntity; 088import org.jfree.chart.event.RendererChangeEvent; 089import org.jfree.chart.labels.XYToolTipGenerator; 090import org.jfree.chart.plot.CrosshairState; 091import org.jfree.chart.plot.PlotOrientation; 092import org.jfree.chart.plot.PlotRenderingInfo; 093import org.jfree.chart.plot.XYPlot; 094import org.jfree.chart.urls.XYURLGenerator; 095import org.jfree.data.Range; 096import org.jfree.data.general.DatasetUtilities; 097import org.jfree.data.xy.TableXYDataset; 098import org.jfree.data.xy.XYDataset; 099import org.jfree.io.SerialUtilities; 100import org.jfree.util.ObjectUtilities; 101import org.jfree.util.PaintUtilities; 102import org.jfree.util.PublicCloneable; 103import org.jfree.util.ShapeUtilities; 104 105/** 106 * A stacked area renderer for the {@link XYPlot} class. 107 * <br><br> 108 * The example shown here is generated by the 109 * <code>StackedXYAreaRendererDemo1.java</code> program included in the 110 * JFreeChart demo collection: 111 * <br><br> 112 * <img src="../../../../../images/StackedXYAreaRendererSample.png" 113 * alt="StackedXYAreaRendererSample.png"> 114 * <br><br> 115 * SPECIAL NOTE: This renderer does not currently handle negative data values 116 * correctly. This should get fixed at some point, but the current workaround 117 * is to use the {@link StackedXYAreaRenderer2} class instead. 118 */ 119public class StackedXYAreaRenderer extends XYAreaRenderer 120 implements Cloneable, PublicCloneable, Serializable { 121 122 /** For serialization. */ 123 private static final long serialVersionUID = 5217394318178570889L; 124 125 /** 126 * A state object for use by this renderer. 127 */ 128 static class StackedXYAreaRendererState extends XYItemRendererState { 129 130 /** The area for the current series. */ 131 private Polygon seriesArea; 132 133 /** The line. */ 134 private Line2D line; 135 136 /** The points from the last series. */ 137 private Stack lastSeriesPoints; 138 139 /** The points for the current series. */ 140 private Stack currentSeriesPoints; 141 142 /** 143 * Creates a new state for the renderer. 144 * 145 * @param info the plot rendering info. 146 */ 147 public StackedXYAreaRendererState(PlotRenderingInfo info) { 148 super(info); 149 this.seriesArea = null; 150 this.line = new Line2D.Double(); 151 this.lastSeriesPoints = new Stack(); 152 this.currentSeriesPoints = new Stack(); 153 } 154 155 /** 156 * Returns the series area. 157 * 158 * @return The series area. 159 */ 160 public Polygon getSeriesArea() { 161 return this.seriesArea; 162 } 163 164 /** 165 * Sets the series area. 166 * 167 * @param area the area. 168 */ 169 public void setSeriesArea(Polygon area) { 170 this.seriesArea = area; 171 } 172 173 /** 174 * Returns the working line. 175 * 176 * @return The working line. 177 */ 178 public Line2D getLine() { 179 return this.line; 180 } 181 182 /** 183 * Returns the current series points. 184 * 185 * @return The current series points. 186 */ 187 public Stack getCurrentSeriesPoints() { 188 return this.currentSeriesPoints; 189 } 190 191 /** 192 * Sets the current series points. 193 * 194 * @param points the points. 195 */ 196 public void setCurrentSeriesPoints(Stack points) { 197 this.currentSeriesPoints = points; 198 } 199 200 /** 201 * Returns the last series points. 202 * 203 * @return The last series points. 204 */ 205 public Stack getLastSeriesPoints() { 206 return this.lastSeriesPoints; 207 } 208 209 /** 210 * Sets the last series points. 211 * 212 * @param points the points. 213 */ 214 public void setLastSeriesPoints(Stack points) { 215 this.lastSeriesPoints = points; 216 } 217 218 } 219 220 /** 221 * Custom Paint for drawing all shapes, if null defaults to series shapes 222 */ 223 private transient Paint shapePaint = null; 224 225 /** 226 * Custom Stroke for drawing all shapes, if null defaults to series 227 * strokes. 228 */ 229 private transient Stroke shapeStroke = null; 230 231 /** 232 * Creates a new renderer. 233 */ 234 public StackedXYAreaRenderer() { 235 this(AREA); 236 } 237 238 /** 239 * Constructs a new renderer. 240 * 241 * @param type the type of the renderer. 242 */ 243 public StackedXYAreaRenderer(int type) { 244 this(type, null, null); 245 } 246 247 /** 248 * Constructs a new renderer. To specify the type of renderer, use one of 249 * the constants: <code>SHAPES</code>, <code>LINES</code>, 250 * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or 251 * <code>AREA_AND_SHAPES</code>. 252 * 253 * @param type the type of renderer. 254 * @param labelGenerator the tool tip generator to use (<code>null</code> 255 * is none). 256 * @param urlGenerator the URL generator (<code>null</code> permitted). 257 */ 258 public StackedXYAreaRenderer(int type, XYToolTipGenerator labelGenerator, 259 XYURLGenerator urlGenerator) { 260 super(type, labelGenerator, urlGenerator); 261 } 262 263 /** 264 * Returns the paint used for rendering shapes, or <code>null</code> if 265 * using series paints. 266 * 267 * @return The paint (possibly <code>null</code>). 268 * 269 * @see #setShapePaint(Paint) 270 */ 271 public Paint getShapePaint() { 272 return this.shapePaint; 273 } 274 275 /** 276 * Sets the paint for rendering shapes and sends a 277 * {@link RendererChangeEvent} to all registered listeners. 278 * 279 * @param shapePaint the paint (<code>null</code> permitted). 280 * 281 * @see #getShapePaint() 282 */ 283 public void setShapePaint(Paint shapePaint) { 284 this.shapePaint = shapePaint; 285 fireChangeEvent(); 286 } 287 288 /** 289 * Returns the stroke used for rendering shapes, or <code>null</code> if 290 * using series strokes. 291 * 292 * @return The stroke (possibly <code>null</code>). 293 * 294 * @see #setShapeStroke(Stroke) 295 */ 296 public Stroke getShapeStroke() { 297 return this.shapeStroke; 298 } 299 300 /** 301 * Sets the stroke for rendering shapes and sends a 302 * {@link RendererChangeEvent} to all registered listeners. 303 * 304 * @param shapeStroke the stroke (<code>null</code> permitted). 305 * 306 * @see #getShapeStroke() 307 */ 308 public void setShapeStroke(Stroke shapeStroke) { 309 this.shapeStroke = shapeStroke; 310 fireChangeEvent(); 311 } 312 313 /** 314 * Initialises the renderer. This method will be called before the first 315 * item is rendered, giving the renderer an opportunity to initialise any 316 * state information it wants to maintain. 317 * 318 * @param g2 the graphics device. 319 * @param dataArea the area inside the axes. 320 * @param plot the plot. 321 * @param data the data. 322 * @param info an optional info collection object to return data back to 323 * the caller. 324 * 325 * @return A state object that should be passed to subsequent calls to the 326 * drawItem() method. 327 */ 328 @Override 329 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 330 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 331 332 XYItemRendererState state = new StackedXYAreaRendererState(info); 333 // in the rendering process, there is special handling for item 334 // zero, so we can't support processing of visible data items only 335 state.setProcessVisibleItemsOnly(false); 336 return state; 337 } 338 339 /** 340 * Returns the number of passes required by the renderer. 341 * 342 * @return 2. 343 */ 344 @Override 345 public int getPassCount() { 346 return 2; 347 } 348 349 /** 350 * Returns the range of values the renderer requires to display all the 351 * items from the specified dataset. 352 * 353 * @param dataset the dataset (<code>null</code> permitted). 354 * 355 * @return The range ([0.0, 0.0] if the dataset contains no values, and 356 * <code>null</code> if the dataset is <code>null</code>). 357 * 358 * @throws ClassCastException if <code>dataset</code> is not an instance 359 * of {@link TableXYDataset}. 360 */ 361 @Override 362 public Range findRangeBounds(XYDataset dataset) { 363 if (dataset != null) { 364 return DatasetUtilities.findStackedRangeBounds( 365 (TableXYDataset) dataset); 366 } 367 else { 368 return null; 369 } 370 } 371 372 /** 373 * Draws the visual representation of a single data item. 374 * 375 * @param g2 the graphics device. 376 * @param state the renderer state. 377 * @param dataArea the area within which the data is being drawn. 378 * @param info collects information about the drawing. 379 * @param plot the plot (can be used to obtain standard color information 380 * etc). 381 * @param domainAxis the domain axis. 382 * @param rangeAxis the range axis. 383 * @param dataset the dataset. 384 * @param series the series index (zero-based). 385 * @param item the item index (zero-based). 386 * @param crosshairState information about crosshairs on a plot. 387 * @param pass the pass index. 388 * 389 * @throws ClassCastException if <code>state</code> is not an instance of 390 * <code>StackedXYAreaRendererState</code> or <code>dataset</code> 391 * is not an instance of {@link TableXYDataset}. 392 */ 393 @Override 394 public void drawItem(Graphics2D g2, XYItemRendererState state, 395 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 396 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 397 int series, int item, CrosshairState crosshairState, int pass) { 398 399 PlotOrientation orientation = plot.getOrientation(); 400 StackedXYAreaRendererState areaState 401 = (StackedXYAreaRendererState) state; 402 // Get the item count for the series, so that we can know which is the 403 // end of the series. 404 TableXYDataset tdataset = (TableXYDataset) dataset; 405 int itemCount = tdataset.getItemCount(); 406 407 // get the data point... 408 double x1 = dataset.getXValue(series, item); 409 double y1 = dataset.getYValue(series, item); 410 boolean nullPoint = false; 411 if (Double.isNaN(y1)) { 412 y1 = 0.0; 413 nullPoint = true; 414 } 415 416 // Get height adjustment based on stack and translate to Java2D values 417 double ph1 = getPreviousHeight(tdataset, series, item); 418 double transX1 = domainAxis.valueToJava2D(x1, dataArea, 419 plot.getDomainAxisEdge()); 420 double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea, 421 plot.getRangeAxisEdge()); 422 423 // Get series Paint and Stroke 424 Paint seriesPaint = getItemPaint(series, item); 425 Paint seriesFillPaint = seriesPaint; 426 if (getUseFillPaint()) { 427 seriesFillPaint = getItemFillPaint(series, item); 428 } 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.setPaint(seriesPaint); 488 g2.setStroke(seriesStroke); 489 g2.draw(areaState.getLine()); 490 } 491 } 492 493 // Check if the item is the last item for the series and number of 494 // items > 0. We can't draw an area for a single point. 495 if (getPlotArea() && item > 0 && item == (itemCount - 1)) { 496 497 double transY2 = rangeAxis.valueToJava2D(ph1, dataArea, 498 plot.getRangeAxisEdge()); 499 500 if (orientation == PlotOrientation.VERTICAL) { 501 // Add the last point (x,0) 502 areaState.getSeriesArea().addPoint((int) transX1, 503 (int) transY2); 504 } 505 else if (orientation == PlotOrientation.HORIZONTAL) { 506 // Add the last point (x,0) 507 areaState.getSeriesArea().addPoint((int) transY2, 508 (int) transX1); 509 } 510 511 // Add points from last series to complete the base of the 512 // polygon 513 if (series != 0) { 514 Stack points = areaState.getLastSeriesPoints(); 515 while (!points.empty()) { 516 Point point = (Point) points.pop(); 517 areaState.getSeriesArea().addPoint((int) point.getX(), 518 (int) point.getY()); 519 } 520 } 521 522 // Fill the polygon 523 g2.setPaint(seriesFillPaint); 524 g2.setStroke(seriesStroke); 525 g2.fill(areaState.getSeriesArea()); 526 527 // Draw an outline around the Area. 528 if (isOutline()) { 529 g2.setStroke(lookupSeriesOutlineStroke(series)); 530 g2.setPaint(lookupSeriesOutlinePaint(series)); 531 g2.draw(areaState.getSeriesArea()); 532 } 533 } 534 535 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 536 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 537 updateCrosshairValues(crosshairState, x1, ph1 + y1, domainAxisIndex, 538 rangeAxisIndex, transX1, transY1, orientation); 539 540 } 541 else if (pass == 1) { 542 // On second pass render shapes and collect entity and tooltip 543 // information 544 545 Shape shape = null; 546 if (getPlotShapes()) { 547 shape = getItemShape(series, item); 548 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 549 shape = ShapeUtilities.createTranslatedShape(shape, 550 transX1, transY1); 551 } 552 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 553 shape = ShapeUtilities.createTranslatedShape(shape, 554 transY1, transX1); 555 } 556 if (!nullPoint) { 557 if (getShapePaint() != null) { 558 g2.setPaint(getShapePaint()); 559 } 560 else { 561 g2.setPaint(seriesPaint); 562 } 563 if (getShapeStroke() != null) { 564 g2.setStroke(getShapeStroke()); 565 } 566 else { 567 g2.setStroke(seriesStroke); 568 } 569 g2.draw(shape); 570 } 571 } 572 else { 573 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 574 shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3, 575 6.0, 6.0); 576 } 577 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 578 shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3, 579 6.0, 6.0); 580 } 581 } 582 583 // collect entity and tool tip information... 584 if (state.getInfo() != null) { 585 EntityCollection entities = state.getEntityCollection(); 586 if (entities != null && shape != null && !nullPoint) { 587 String tip = null; 588 XYToolTipGenerator generator 589 = getToolTipGenerator(series, item); 590 if (generator != null) { 591 tip = generator.generateToolTip(dataset, series, item); 592 } 593 String url = null; 594 if (getURLGenerator() != null) { 595 url = getURLGenerator().generateURL(dataset, series, 596 item); 597 } 598 XYItemEntity entity = new XYItemEntity(shape, dataset, 599 series, item, tip, url); 600 entities.add(entity); 601 } 602 } 603 604 } 605 } 606 607 /** 608 * Calculates the stacked value of the all series up to, but not including 609 * <code>series</code> for the specified item. It returns 0.0 if 610 * <code>series</code> is the first series, i.e. 0. 611 * 612 * @param dataset the dataset. 613 * @param series the series. 614 * @param index the index. 615 * 616 * @return The cumulative value for all series' values up to but excluding 617 * <code>series</code> for <code>index</code>. 618 */ 619 protected double getPreviousHeight(TableXYDataset dataset, 620 int series, int index) { 621 double result = 0.0; 622 for (int i = 0; i < series; i++) { 623 double value = dataset.getYValue(i, index); 624 if (!Double.isNaN(value)) { 625 result += value; 626 } 627 } 628 return result; 629 } 630 631 /** 632 * Tests the renderer for equality with an arbitrary object. 633 * 634 * @param obj the object (<code>null</code> permitted). 635 * 636 * @return A boolean. 637 */ 638 @Override 639 public boolean equals(Object obj) { 640 if (obj == this) { 641 return true; 642 } 643 if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) { 644 return false; 645 } 646 StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj; 647 if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) { 648 return false; 649 } 650 if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) { 651 return false; 652 } 653 return true; 654 } 655 656 /** 657 * Returns a clone of the renderer. 658 * 659 * @return A clone. 660 * 661 * @throws CloneNotSupportedException if the renderer cannot be cloned. 662 */ 663 @Override 664 public Object clone() throws CloneNotSupportedException { 665 return super.clone(); 666 } 667 668 /** 669 * Provides serialization support. 670 * 671 * @param stream the input stream. 672 * 673 * @throws IOException if there is an I/O error. 674 * @throws ClassNotFoundException if there is a classpath problem. 675 */ 676 private void readObject(ObjectInputStream stream) 677 throws IOException, ClassNotFoundException { 678 stream.defaultReadObject(); 679 this.shapePaint = SerialUtilities.readPaint(stream); 680 this.shapeStroke = SerialUtilities.readStroke(stream); 681 } 682 683 /** 684 * Provides serialization support. 685 * 686 * @param stream the output stream. 687 * 688 * @throws IOException if there is an I/O error. 689 */ 690 private void writeObject(ObjectOutputStream stream) throws IOException { 691 stream.defaultWriteObject(); 692 SerialUtilities.writePaint(this.shapePaint, stream); 693 SerialUtilities.writeStroke(this.shapeStroke, stream); 694 } 695 696}