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