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 * XYStepAreaRenderer.java 029 * ----------------------- 030 * (C) Copyright 2003-2014, by Matthias Rose and Contributors. 031 * 032 * Original Author: Matthias Rose (based on XYAreaRenderer.java); 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Lukasz Rzeszotarski; 035 * 036 * Changes: 037 * -------- 038 * 07-Oct-2003 : Version 1, contributed by Matthias Rose (DG); 039 * 10-Feb-2004 : Added some getter and setter methods (DG); 040 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed 041 * XYToolTipGenerator --> XYItemLabelGenerator (DG); 042 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 043 * getYValue() (DG); 044 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 045 * 06-Jul-2005 : Renamed get/setPlotShapes() --> get/setShapesVisible() (DG); 046 * ------------- JFREECHART 1.0.x --------------------------------------------- 047 * 06-Jul-2006 : Modified to call dataset methods that return double 048 * primitives only (DG); 049 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 050 * 14-Feb-2007 : Added equals() method override (DG); 051 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG); 052 * 14-May-2008 : Call addEntity() from within drawItem() (DG); 053 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG); 054 * 05-Dec-2013 : Added setStepPoint() method (LR); 055 * 056 */ 057 058package org.jfree.chart.renderer.xy; 059 060import java.awt.Graphics2D; 061import java.awt.Paint; 062import java.awt.Polygon; 063import java.awt.Shape; 064import java.awt.Stroke; 065import java.awt.geom.Rectangle2D; 066import java.io.Serializable; 067 068import org.jfree.chart.axis.ValueAxis; 069import org.jfree.chart.entity.EntityCollection; 070import org.jfree.chart.event.RendererChangeEvent; 071import org.jfree.chart.labels.XYToolTipGenerator; 072import org.jfree.chart.plot.CrosshairState; 073import org.jfree.chart.plot.PlotOrientation; 074import org.jfree.chart.plot.PlotRenderingInfo; 075import org.jfree.chart.plot.XYPlot; 076import org.jfree.chart.urls.XYURLGenerator; 077import org.jfree.data.xy.XYDataset; 078import org.jfree.util.PublicCloneable; 079import org.jfree.util.ShapeUtilities; 080 081/** 082 * A step chart renderer that fills the area between the step and the x-axis. 083 * The example shown here is generated by the 084 * <code>XYStepAreaRendererDemo1.java</code> program included in the JFreeChart 085 * demo collection: 086 * <br><br> 087 * <img src="../../../../../images/XYStepAreaRendererSample.png" 088 * alt="XYStepAreaRendererSample.png"> 089 */ 090public class XYStepAreaRenderer extends AbstractXYItemRenderer 091 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 092 093 /** For serialization. */ 094 private static final long serialVersionUID = -7311560779702649635L; 095 096 /** Useful constant for specifying the type of rendering (shapes only). */ 097 public static final int SHAPES = 1; 098 099 /** Useful constant for specifying the type of rendering (area only). */ 100 public static final int AREA = 2; 101 102 /** 103 * Useful constant for specifying the type of rendering (area and shapes). 104 */ 105 public static final int AREA_AND_SHAPES = 3; 106 107 /** A flag indicating whether or not shapes are drawn at each XY point. */ 108 private boolean shapesVisible; 109 110 /** A flag that controls whether or not shapes are filled for ALL series. */ 111 private boolean shapesFilled; 112 113 /** A flag indicating whether or not Area are drawn at each XY point. */ 114 private boolean plotArea; 115 116 /** A flag that controls whether or not the outline is shown. */ 117 private boolean showOutline; 118 119 /** Area of the complete series */ 120 protected transient Polygon pArea = null; 121 122 /** 123 * The value on the range axis which defines the 'lower' border of the 124 * area. 125 */ 126 private double rangeBase; 127 128 /** 129 * The factor (from 0.0 to 1.0) that determines the position of the 130 * step. 131 * 132 * @since 1.0.18. 133 */ 134 private double stepPoint; 135 136 /** 137 * Constructs a new renderer. 138 */ 139 public XYStepAreaRenderer() { 140 this(AREA); 141 } 142 143 /** 144 * Constructs a new renderer. 145 * 146 * @param type the type of the renderer. 147 */ 148 public XYStepAreaRenderer(int type) { 149 this(type, null, null); 150 } 151 152 /** 153 * Constructs a new renderer. 154 * <p> 155 * To specify the type of renderer, use one of the constants: 156 * AREA, SHAPES or AREA_AND_SHAPES. 157 * 158 * @param type the type of renderer. 159 * @param toolTipGenerator the tool tip generator to use 160 * (<code>null</code> permitted). 161 * @param urlGenerator the URL generator (<code>null</code> permitted). 162 */ 163 public XYStepAreaRenderer(int type, XYToolTipGenerator toolTipGenerator, 164 XYURLGenerator urlGenerator) { 165 super(); 166 setBaseToolTipGenerator(toolTipGenerator); 167 setURLGenerator(urlGenerator); 168 169 if (type == AREA) { 170 this.plotArea = true; 171 } 172 else if (type == SHAPES) { 173 this.shapesVisible = true; 174 } 175 else if (type == AREA_AND_SHAPES) { 176 this.plotArea = true; 177 this.shapesVisible = true; 178 } 179 this.showOutline = false; 180 this.stepPoint = 1.0; 181 } 182 183 /** 184 * Returns a flag that controls whether or not outlines of the areas are 185 * drawn. 186 * 187 * @return The flag. 188 * 189 * @see #setOutline(boolean) 190 */ 191 public boolean isOutline() { 192 return this.showOutline; 193 } 194 195 /** 196 * Sets a flag that controls whether or not outlines of the areas are 197 * drawn, and sends a {@link RendererChangeEvent} to all registered 198 * listeners. 199 * 200 * @param show the flag. 201 * 202 * @see #isOutline() 203 */ 204 public void setOutline(boolean show) { 205 this.showOutline = show; 206 fireChangeEvent(); 207 } 208 209 /** 210 * Returns true if shapes are being plotted by the renderer. 211 * 212 * @return <code>true</code> if shapes are being plotted by the renderer. 213 * 214 * @see #setShapesVisible(boolean) 215 */ 216 public boolean getShapesVisible() { 217 return this.shapesVisible; 218 } 219 220 /** 221 * Sets the flag that controls whether or not shapes are displayed for each 222 * data item, and sends a {@link RendererChangeEvent} to all registered 223 * listeners. 224 * 225 * @param flag the flag. 226 * 227 * @see #getShapesVisible() 228 */ 229 public void setShapesVisible(boolean flag) { 230 this.shapesVisible = flag; 231 fireChangeEvent(); 232 } 233 234 /** 235 * Returns the flag that controls whether or not the shapes are filled. 236 * 237 * @return A boolean. 238 * 239 * @see #setShapesFilled(boolean) 240 */ 241 public boolean isShapesFilled() { 242 return this.shapesFilled; 243 } 244 245 /** 246 * Sets the 'shapes filled' for ALL series and sends a 247 * {@link RendererChangeEvent} to all registered listeners. 248 * 249 * @param filled the flag. 250 * 251 * @see #isShapesFilled() 252 */ 253 public void setShapesFilled(boolean filled) { 254 this.shapesFilled = filled; 255 fireChangeEvent(); 256 } 257 258 /** 259 * Returns true if Area is being plotted by the renderer. 260 * 261 * @return <code>true</code> if Area is being plotted by the renderer. 262 * 263 * @see #setPlotArea(boolean) 264 */ 265 public boolean getPlotArea() { 266 return this.plotArea; 267 } 268 269 /** 270 * Sets a flag that controls whether or not areas are drawn for each data 271 * item and sends a {@link RendererChangeEvent} to all registered 272 * listeners. 273 * 274 * @param flag the flag. 275 * 276 * @see #getPlotArea() 277 */ 278 public void setPlotArea(boolean flag) { 279 this.plotArea = flag; 280 fireChangeEvent(); 281 } 282 283 /** 284 * Returns the value on the range axis which defines the 'lower' border of 285 * the area. 286 * 287 * @return <code>double</code> the value on the range axis which defines 288 * the 'lower' border of the area. 289 * 290 * @see #setRangeBase(double) 291 */ 292 public double getRangeBase() { 293 return this.rangeBase; 294 } 295 296 /** 297 * Sets the value on the range axis which defines the default border of the 298 * area, and sends a {@link RendererChangeEvent} to all registered 299 * listeners. E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 300 * reach the lower border of the plotArea. 301 * 302 * @param val the value on the range axis which defines the default border 303 * of the area. 304 * 305 * @see #getRangeBase() 306 */ 307 public void setRangeBase(double val) { 308 this.rangeBase = val; 309 fireChangeEvent(); 310 } 311 312 /** 313 * Returns the fraction of the domain position between two points on which 314 * the step is drawn. The default is 1.0d, which means the step is drawn 315 * at the domain position of the second`point. If the stepPoint is 0.5d the 316 * step is drawn at half between the two points. 317 * 318 * @return The fraction of the domain position between two points where the 319 * step is drawn. 320 * 321 * @see #setStepPoint(double) 322 * 323 * @since 1.0.18 324 */ 325 public double getStepPoint() { 326 return stepPoint; 327 } 328 329 /** 330 * Sets the step point and sends a {@link RendererChangeEvent} to all 331 * registered listeners. 332 * 333 * @param stepPoint the step point (in the range 0.0 to 1.0) 334 * 335 * @see #getStepPoint() 336 * 337 * @since 1.0.18 338 */ 339 public void setStepPoint(double stepPoint) { 340 if (stepPoint < 0.0d || stepPoint > 1.0d) { 341 throw new IllegalArgumentException( 342 "Requires stepPoint in [0.0;1.0]"); 343 } 344 this.stepPoint = stepPoint; 345 fireChangeEvent(); 346 } 347 348 /** 349 * Initialises the renderer. Here we calculate the Java2D y-coordinate for 350 * zero, since all the bars have their bases fixed at zero. 351 * 352 * @param g2 the graphics device. 353 * @param dataArea the area inside the axes. 354 * @param plot the plot. 355 * @param data the data. 356 * @param info an optional info collection object to return data back to 357 * the caller. 358 * 359 * @return The number of passes required by the renderer. 360 */ 361 @Override 362 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 363 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 364 365 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 366 info); 367 // disable visible items optimisation - it doesn't work for this 368 // renderer... 369 state.setProcessVisibleItemsOnly(false); 370 return state; 371 372 } 373 374 /** 375 * Draws the visual representation of a single data item. 376 * 377 * @param g2 the graphics device. 378 * @param state the renderer state. 379 * @param dataArea the area within which the data is being drawn. 380 * @param info collects information about the drawing. 381 * @param plot the plot (can be used to obtain standard color information 382 * etc). 383 * @param domainAxis the domain axis. 384 * @param rangeAxis the range axis. 385 * @param dataset the dataset. 386 * @param series the series index (zero-based). 387 * @param item the item index (zero-based). 388 * @param crosshairState crosshair information for the plot 389 * (<code>null</code> permitted). 390 * @param pass the pass index. 391 */ 392 @Override 393 public void drawItem(Graphics2D g2, XYItemRendererState state, 394 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 395 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 396 int series, int item, CrosshairState crosshairState, int pass) { 397 398 PlotOrientation orientation = plot.getOrientation(); 399 400 // Get the item count for the series, so that we can know which is the 401 // end of the series. 402 int itemCount = dataset.getItemCount(series); 403 404 Paint paint = getItemPaint(series, item); 405 Stroke seriesStroke = getItemStroke(series, item); 406 g2.setPaint(paint); 407 g2.setStroke(seriesStroke); 408 409 // get the data point... 410 double x1 = dataset.getXValue(series, item); 411 double y1 = dataset.getYValue(series, item); 412 double x = x1; 413 double y = Double.isNaN(y1) ? getRangeBase() : y1; 414 double transX1 = domainAxis.valueToJava2D(x, dataArea, 415 plot.getDomainAxisEdge()); 416 double transY1 = rangeAxis.valueToJava2D(y, dataArea, 417 plot.getRangeAxisEdge()); 418 419 // avoid possible sun.dc.pr.PRException: endPath: bad path 420 transY1 = restrictValueToDataArea(transY1, plot, dataArea); 421 422 if (this.pArea == null && !Double.isNaN(y1)) { 423 424 // Create a new Area for the series 425 this.pArea = new Polygon(); 426 427 // start from Y = rangeBase 428 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 429 plot.getRangeAxisEdge()); 430 431 // avoid possible sun.dc.pr.PRException: endPath: bad path 432 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 433 434 // The first point is (x, this.baseYValue) 435 if (orientation == PlotOrientation.VERTICAL) { 436 this.pArea.addPoint((int) transX1, (int) transY2); 437 } 438 else if (orientation == PlotOrientation.HORIZONTAL) { 439 this.pArea.addPoint((int) transY2, (int) transX1); 440 } 441 } 442 443 double transX0; 444 double transY0; 445 446 double x0; 447 double y0; 448 if (item > 0) { 449 // get the previous data point... 450 x0 = dataset.getXValue(series, item - 1); 451 y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1); 452 453 x = x0; 454 y = Double.isNaN(y0) ? getRangeBase() : y0; 455 transX0 = domainAxis.valueToJava2D(x, dataArea, 456 plot.getDomainAxisEdge()); 457 transY0 = rangeAxis.valueToJava2D(y, dataArea, 458 plot.getRangeAxisEdge()); 459 460 // avoid possible sun.dc.pr.PRException: endPath: bad path 461 transY0 = restrictValueToDataArea(transY0, plot, dataArea); 462 463 if (Double.isNaN(y1)) { 464 // NULL value -> insert point on base line 465 // instead of 'step point' 466 transX1 = transX0; 467 transY0 = transY1; 468 } 469 if (transY0 != transY1) { 470 // not just a horizontal bar but need to perform a 'step'. 471 double transXs = transX0 + (getStepPoint() 472 * (transX1 - transX0)); 473 if (orientation == PlotOrientation.VERTICAL) { 474 this.pArea.addPoint((int) transXs, (int) transY0); 475 this.pArea.addPoint((int) transXs, (int) transY1); 476 } 477 else if (orientation == PlotOrientation.HORIZONTAL) { 478 this.pArea.addPoint((int) transY0, (int) transXs); 479 this.pArea.addPoint((int) transY1, (int) transXs); 480 } 481 } 482 } 483 484 Shape shape = null; 485 if (!Double.isNaN(y1)) { 486 // Add each point to Area (x, y) 487 if (orientation == PlotOrientation.VERTICAL) { 488 this.pArea.addPoint((int) transX1, (int) transY1); 489 } 490 else if (orientation == PlotOrientation.HORIZONTAL) { 491 this.pArea.addPoint((int) transY1, (int) transX1); 492 } 493 494 if (getShapesVisible()) { 495 shape = getItemShape(series, item); 496 if (orientation == PlotOrientation.VERTICAL) { 497 shape = ShapeUtilities.createTranslatedShape(shape, 498 transX1, transY1); 499 } 500 else if (orientation == PlotOrientation.HORIZONTAL) { 501 shape = ShapeUtilities.createTranslatedShape(shape, 502 transY1, transX1); 503 } 504 if (isShapesFilled()) { 505 g2.fill(shape); 506 } 507 else { 508 g2.draw(shape); 509 } 510 } 511 else { 512 if (orientation == PlotOrientation.VERTICAL) { 513 shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 514 4.0, 4.0); 515 } 516 else if (orientation == PlotOrientation.HORIZONTAL) { 517 shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 518 4.0, 4.0); 519 } 520 } 521 } 522 523 // Check if the item is the last item for the series or if it 524 // is a NULL value and number of items > 0. We can't draw an area for 525 // a single point. 526 if (getPlotArea() && item > 0 && this.pArea != null 527 && (item == (itemCount - 1) || Double.isNaN(y1))) { 528 529 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 530 plot.getRangeAxisEdge()); 531 532 // avoid possible sun.dc.pr.PRException: endPath: bad path 533 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 534 535 if (orientation == PlotOrientation.VERTICAL) { 536 // Add the last point (x,0) 537 this.pArea.addPoint((int) transX1, (int) transY2); 538 } 539 else if (orientation == PlotOrientation.HORIZONTAL) { 540 // Add the last point (x,0) 541 this.pArea.addPoint((int) transY2, (int) transX1); 542 } 543 544 // fill the polygon 545 g2.fill(this.pArea); 546 547 // draw an outline around the Area. 548 if (isOutline()) { 549 g2.setStroke(plot.getOutlineStroke()); 550 g2.setPaint(plot.getOutlinePaint()); 551 g2.draw(this.pArea); 552 } 553 554 // start new area when needed (see above) 555 this.pArea = null; 556 } 557 558 // do we need to update the crosshair values? 559 if (!Double.isNaN(y1)) { 560 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 561 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 562 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 563 rangeAxisIndex, transX1, transY1, orientation); 564 } 565 566 // collect entity and tool tip information... 567 EntityCollection entities = state.getEntityCollection(); 568 if (entities != null) { 569 addEntity(entities, shape, dataset, series, item, transX1, transY1); 570 } 571 } 572 573 /** 574 * Tests this renderer for equality with an arbitrary object. 575 * 576 * @param obj the object (<code>null</code> permitted). 577 * 578 * @return A boolean. 579 */ 580 @Override 581 public boolean equals(Object obj) { 582 if (obj == this) { 583 return true; 584 } 585 if (!(obj instanceof XYStepAreaRenderer)) { 586 return false; 587 } 588 XYStepAreaRenderer that = (XYStepAreaRenderer) obj; 589 if (this.showOutline != that.showOutline) { 590 return false; 591 } 592 if (this.shapesVisible != that.shapesVisible) { 593 return false; 594 } 595 if (this.shapesFilled != that.shapesFilled) { 596 return false; 597 } 598 if (this.plotArea != that.plotArea) { 599 return false; 600 } 601 if (this.rangeBase != that.rangeBase) { 602 return false; 603 } 604 if (this.stepPoint != that.stepPoint) { 605 return false; 606 } 607 return super.equals(obj); 608 } 609 610 /** 611 * Returns a clone of the renderer. 612 * 613 * @return A clone. 614 * 615 * @throws CloneNotSupportedException if the renderer cannot be cloned. 616 */ 617 @Override 618 public Object clone() throws CloneNotSupportedException { 619 return super.clone(); 620 } 621 622 /** 623 * Helper method which returns a value if it lies 624 * inside the visible dataArea and otherwise the corresponding 625 * coordinate on the border of the dataArea. The PlotOrientation 626 * is taken into account. 627 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path 628 * which occurs when trying to draw lines/shapes which in large part 629 * lie outside of the visible dataArea. 630 * 631 * @param value the value which shall be 632 * @param dataArea the area within which the data is being drawn. 633 * @param plot the plot (can be used to obtain standard color 634 * information etc). 635 * @return <code>double</code> value inside the data area. 636 */ 637 protected static double restrictValueToDataArea(double value, 638 XYPlot plot, 639 Rectangle2D dataArea) { 640 double min = 0; 641 double max = 0; 642 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 643 min = dataArea.getMinY(); 644 max = dataArea.getMaxY(); 645 } 646 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 647 min = dataArea.getMinX(); 648 max = dataArea.getMaxX(); 649 } 650 if (value < min) { 651 value = min; 652 } 653 else if (value > max) { 654 value = max; 655 } 656 return value; 657 } 658 659}