1 /* =========================================================== 2 * JFreeChart : a free chart library for the Java(tm) platform 3 * =========================================================== 4 * 5 * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors. 6 * 7 * Project Info: http://www.jfree.org/jfreechart/index.html 8 * 9 * This library is free software; you can redistribute it and/or modify it 10 * under the terms of the GNU Lesser General Public License as published by 11 * the Free Software Foundation; either version 2.1 of the License, or 12 * (at your option) any later version. 13 * 14 * This library is distributed in the hope that it will be useful, but 15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 16 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 17 * License for more details. 18 * 19 * You should have received a copy of the GNU Lesser General Public 20 * License along with this library; if not, write to the Free Software 21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 22 * USA. 23 * 24 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 25 * Other names may be trademarks of their respective owners.] 26 * 27 * ----------------------- 28 * XYStepAreaRenderer.java 29 * ----------------------- 30 * (C) Copyright 2003-2009, by Matthias Rose and Contributors. 31 * 32 * Original Author: Matthias Rose (based on XYAreaRenderer.java); 33 * Contributor(s): David Gilbert (for Object Refinery Limited); 34 * 35 * Changes: 36 * -------- 37 * 07-Oct-2003 : Version 1, contributed by Matthias Rose (DG); 38 * 10-Feb-2004 : Added some getter and setter methods (DG); 39 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed 40 * XYToolTipGenerator --> XYItemLabelGenerator (DG); 41 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 42 * getYValue() (DG); 43 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 44 * 06-Jul-2005 : Renamed get/setPlotShapes() --> get/setShapesVisible() (DG); 45 * ------------- JFREECHART 1.0.x --------------------------------------------- 46 * 06-Jul-2006 : Modified to call dataset methods that return double 47 * primitives only (DG); 48 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 49 * 14-Feb-2007 : Added equals() method override (DG); 50 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG); 51 * 14-May-2008 : Call addEntity() from within drawItem() (DG); 52 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG); 53 * 54 */ 55 56 package org.jfree.chart.renderer.xy; 57 58 import java.awt.Graphics2D; 59 import java.awt.Paint; 60 import java.awt.Polygon; 61 import java.awt.Shape; 62 import java.awt.Stroke; 63 import java.awt.geom.Rectangle2D; 64 import java.io.Serializable; 65 66 import org.jfree.chart.axis.ValueAxis; 67 import org.jfree.chart.entity.EntityCollection; 68 import org.jfree.chart.event.RendererChangeEvent; 69 import org.jfree.chart.labels.XYToolTipGenerator; 70 import org.jfree.chart.plot.CrosshairState; 71 import org.jfree.chart.plot.PlotOrientation; 72 import org.jfree.chart.plot.PlotRenderingInfo; 73 import org.jfree.chart.plot.XYPlot; 74 import org.jfree.chart.urls.XYURLGenerator; 75 import org.jfree.data.xy.XYDataset; 76 import org.jfree.util.PublicCloneable; 77 import org.jfree.util.ShapeUtilities; 78 79 /** 80 * A step chart renderer that fills the area between the step and the x-axis. 81 * The example shown here is generated by the 82 * <code>XYStepAreaRendererDemo1.java</code> program included in the JFreeChart 83 * demo collection: 84 * <br><br> 85 * <img src="../../../../../images/XYStepAreaRendererSample.png" 86 * alt="XYStepAreaRendererSample.png" /> 87 */ 88 public class XYStepAreaRenderer extends AbstractXYItemRenderer 89 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 90 91 /** For serialization. */ 92 private static final long serialVersionUID = -7311560779702649635L; 93 94 /** Useful constant for specifying the type of rendering (shapes only). */ 95 public static final int SHAPES = 1; 96 97 /** Useful constant for specifying the type of rendering (area only). */ 98 public static final int AREA = 2; 99 100 /** 101 * Useful constant for specifying the type of rendering (area and shapes). 102 */ 103 public static final int AREA_AND_SHAPES = 3; 104 105 /** A flag indicating whether or not shapes are drawn at each XY point. */ 106 private boolean shapesVisible; 107 108 /** A flag that controls whether or not shapes are filled for ALL series. */ 109 private boolean shapesFilled; 110 111 /** A flag indicating whether or not Area are drawn at each XY point. */ 112 private boolean plotArea; 113 114 /** A flag that controls whether or not the outline is shown. */ 115 private boolean showOutline; 116 117 /** Area of the complete series */ 118 protected transient Polygon pArea = null; 119 120 /** 121 * The value on the range axis which defines the 'lower' border of the 122 * area. 123 */ 124 private double rangeBase; 125 126 /** 127 * Constructs a new renderer. 128 */ XYStepAreaRenderer()129 public XYStepAreaRenderer() { 130 this(AREA); 131 } 132 133 /** 134 * Constructs a new renderer. 135 * 136 * @param type the type of the renderer. 137 */ XYStepAreaRenderer(int type)138 public XYStepAreaRenderer(int type) { 139 this(type, null, null); 140 } 141 142 /** 143 * Constructs a new renderer. 144 * <p> 145 * To specify the type of renderer, use one of the constants: 146 * AREA, SHAPES or AREA_AND_SHAPES. 147 * 148 * @param type the type of renderer. 149 * @param toolTipGenerator the tool tip generator to use 150 * (<code>null</code> permitted). 151 * @param urlGenerator the URL generator (<code>null</code> permitted). 152 */ XYStepAreaRenderer(int type, XYToolTipGenerator toolTipGenerator, XYURLGenerator urlGenerator)153 public XYStepAreaRenderer(int type, 154 XYToolTipGenerator toolTipGenerator, 155 XYURLGenerator urlGenerator) { 156 157 super(); 158 setBaseToolTipGenerator(toolTipGenerator); 159 setURLGenerator(urlGenerator); 160 161 if (type == AREA) { 162 this.plotArea = true; 163 } 164 else if (type == SHAPES) { 165 this.shapesVisible = true; 166 } 167 else if (type == AREA_AND_SHAPES) { 168 this.plotArea = true; 169 this.shapesVisible = true; 170 } 171 this.showOutline = false; 172 } 173 174 /** 175 * Returns a flag that controls whether or not outlines of the areas are 176 * drawn. 177 * 178 * @return The flag. 179 * 180 * @see #setOutline(boolean) 181 */ isOutline()182 public boolean isOutline() { 183 return this.showOutline; 184 } 185 186 /** 187 * Sets a flag that controls whether or not outlines of the areas are 188 * drawn, and sends a {@link RendererChangeEvent} to all registered 189 * listeners. 190 * 191 * @param show the flag. 192 * 193 * @see #isOutline() 194 */ setOutline(boolean show)195 public void setOutline(boolean show) { 196 this.showOutline = show; 197 fireChangeEvent(); 198 } 199 200 /** 201 * Returns true if shapes are being plotted by the renderer. 202 * 203 * @return <code>true</code> if shapes are being plotted by the renderer. 204 * 205 * @see #setShapesVisible(boolean) 206 */ getShapesVisible()207 public boolean getShapesVisible() { 208 return this.shapesVisible; 209 } 210 211 /** 212 * Sets the flag that controls whether or not shapes are displayed for each 213 * data item, and sends a {@link RendererChangeEvent} to all registered 214 * listeners. 215 * 216 * @param flag the flag. 217 * 218 * @see #getShapesVisible() 219 */ setShapesVisible(boolean flag)220 public void setShapesVisible(boolean flag) { 221 this.shapesVisible = flag; 222 fireChangeEvent(); 223 } 224 225 /** 226 * Returns the flag that controls whether or not the shapes are filled. 227 * 228 * @return A boolean. 229 * 230 * @see #setShapesFilled(boolean) 231 */ isShapesFilled()232 public boolean isShapesFilled() { 233 return this.shapesFilled; 234 } 235 236 /** 237 * Sets the 'shapes filled' for ALL series and sends a 238 * {@link RendererChangeEvent} to all registered listeners. 239 * 240 * @param filled the flag. 241 * 242 * @see #isShapesFilled() 243 */ setShapesFilled(boolean filled)244 public void setShapesFilled(boolean filled) { 245 this.shapesFilled = filled; 246 fireChangeEvent(); 247 } 248 249 /** 250 * Returns true if Area is being plotted by the renderer. 251 * 252 * @return <code>true</code> if Area is being plotted by the renderer. 253 * 254 * @see #setPlotArea(boolean) 255 */ getPlotArea()256 public boolean getPlotArea() { 257 return this.plotArea; 258 } 259 260 /** 261 * Sets a flag that controls whether or not areas are drawn for each data 262 * item and sends a {@link RendererChangeEvent} to all registered 263 * listeners. 264 * 265 * @param flag the flag. 266 * 267 * @see #getPlotArea() 268 */ setPlotArea(boolean flag)269 public void setPlotArea(boolean flag) { 270 this.plotArea = flag; 271 fireChangeEvent(); 272 } 273 274 /** 275 * Returns the value on the range axis which defines the 'lower' border of 276 * the area. 277 * 278 * @return <code>double</code> the value on the range axis which defines 279 * the 'lower' border of the area. 280 * 281 * @see #setRangeBase(double) 282 */ getRangeBase()283 public double getRangeBase() { 284 return this.rangeBase; 285 } 286 287 /** 288 * Sets the value on the range axis which defines the default border of the 289 * area, and sends a {@link RendererChangeEvent} to all registered 290 * listeners. E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 291 * reach the lower border of the plotArea. 292 * 293 * @param val the value on the range axis which defines the default border 294 * of the area. 295 * 296 * @see #getRangeBase() 297 */ setRangeBase(double val)298 public void setRangeBase(double val) { 299 this.rangeBase = val; 300 fireChangeEvent(); 301 } 302 303 /** 304 * Initialises the renderer. Here we calculate the Java2D y-coordinate for 305 * zero, since all the bars have their bases fixed at zero. 306 * 307 * @param g2 the graphics device. 308 * @param dataArea the area inside the axes. 309 * @param plot the plot. 310 * @param data the data. 311 * @param info an optional info collection object to return data back to 312 * the caller. 313 * 314 * @return The number of passes required by the renderer. 315 */ 316 @Override initialise(Graphics2D g2, Rectangle2D dataArea, XYPlot plot, XYDataset data, PlotRenderingInfo info)317 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 318 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 319 320 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 321 info); 322 // disable visible items optimisation - it doesn't work for this 323 // renderer... 324 state.setProcessVisibleItemsOnly(false); 325 return state; 326 327 } 328 329 330 /** 331 * Draws the visual representation of a single data item. 332 * 333 * @param g2 the graphics device. 334 * @param state the renderer state. 335 * @param dataArea the area within which the data is being drawn. 336 * @param info collects information about the drawing. 337 * @param plot the plot (can be used to obtain standard color information 338 * etc). 339 * @param domainAxis the domain axis. 340 * @param rangeAxis the range axis. 341 * @param dataset the dataset. 342 * @param series the series index (zero-based). 343 * @param item the item index (zero-based). 344 * @param crosshairState crosshair information for the plot 345 * (<code>null</code> permitted). 346 * @param pass the pass index. 347 */ 348 @Override drawItem(Graphics2D g2, XYItemRendererState state, Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, int series, int item, CrosshairState crosshairState, int pass)349 public void drawItem(Graphics2D g2, XYItemRendererState state, 350 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 351 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 352 int series, int item, CrosshairState crosshairState, int pass) { 353 354 PlotOrientation orientation = plot.getOrientation(); 355 356 // Get the item count for the series, so that we can know which is the 357 // end of the series. 358 int itemCount = dataset.getItemCount(series); 359 360 Paint paint = getItemPaint(series, item); 361 Stroke seriesStroke = getItemStroke(series, item); 362 g2.setPaint(paint); 363 g2.setStroke(seriesStroke); 364 365 // get the data point... 366 double x1 = dataset.getXValue(series, item); 367 double y1 = dataset.getYValue(series, item); 368 double x = x1; 369 double y = Double.isNaN(y1) ? getRangeBase() : y1; 370 double transX1 = domainAxis.valueToJava2D(x, dataArea, 371 plot.getDomainAxisEdge()); 372 double transY1 = rangeAxis.valueToJava2D(y, dataArea, 373 plot.getRangeAxisEdge()); 374 375 // avoid possible sun.dc.pr.PRException: endPath: bad path 376 transY1 = restrictValueToDataArea(transY1, plot, dataArea); 377 378 if (this.pArea == null && !Double.isNaN(y1)) { 379 380 // Create a new Area for the series 381 this.pArea = new Polygon(); 382 383 // start from Y = rangeBase 384 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 385 plot.getRangeAxisEdge()); 386 387 // avoid possible sun.dc.pr.PRException: endPath: bad path 388 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 389 390 // The first point is (x, this.baseYValue) 391 if (orientation == PlotOrientation.VERTICAL) { 392 this.pArea.addPoint((int) transX1, (int) transY2); 393 } 394 else if (orientation == PlotOrientation.HORIZONTAL) { 395 this.pArea.addPoint((int) transY2, (int) transX1); 396 } 397 } 398 399 double transX0; 400 double transY0; 401 402 double x0; 403 double y0; 404 if (item > 0) { 405 // get the previous data point... 406 x0 = dataset.getXValue(series, item - 1); 407 y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1); 408 409 x = x0; 410 y = Double.isNaN(y0) ? getRangeBase() : y0; 411 transX0 = domainAxis.valueToJava2D(x, dataArea, 412 plot.getDomainAxisEdge()); 413 transY0 = rangeAxis.valueToJava2D(y, dataArea, 414 plot.getRangeAxisEdge()); 415 416 // avoid possible sun.dc.pr.PRException: endPath: bad path 417 transY0 = restrictValueToDataArea(transY0, plot, dataArea); 418 419 if (Double.isNaN(y1)) { 420 // NULL value -> insert point on base line 421 // instead of 'step point' 422 transX1 = transX0; 423 transY0 = transY1; 424 } 425 if (transY0 != transY1) { 426 // not just a horizontal bar but need to perform a 'step'. 427 if (orientation == PlotOrientation.VERTICAL) { 428 this.pArea.addPoint((int) transX1, (int) transY0); 429 } 430 else if (orientation == PlotOrientation.HORIZONTAL) { 431 this.pArea.addPoint((int) transY0, (int) transX1); 432 } 433 } 434 } 435 436 Shape shape = null; 437 if (!Double.isNaN(y1)) { 438 // Add each point to Area (x, y) 439 if (orientation == PlotOrientation.VERTICAL) { 440 this.pArea.addPoint((int) transX1, (int) transY1); 441 } 442 else if (orientation == PlotOrientation.HORIZONTAL) { 443 this.pArea.addPoint((int) transY1, (int) transX1); 444 } 445 446 if (getShapesVisible()) { 447 shape = getItemShape(series, item); 448 if (orientation == PlotOrientation.VERTICAL) { 449 shape = ShapeUtilities.createTranslatedShape(shape, 450 transX1, transY1); 451 } 452 else if (orientation == PlotOrientation.HORIZONTAL) { 453 shape = ShapeUtilities.createTranslatedShape(shape, 454 transY1, transX1); 455 } 456 if (isShapesFilled()) { 457 g2.fill(shape); 458 } 459 else { 460 g2.draw(shape); 461 } 462 } 463 else { 464 if (orientation == PlotOrientation.VERTICAL) { 465 shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 466 4.0, 4.0); 467 } 468 else if (orientation == PlotOrientation.HORIZONTAL) { 469 shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 470 4.0, 4.0); 471 } 472 } 473 } 474 475 // Check if the item is the last item for the series or if it 476 // is a NULL value and number of items > 0. We can't draw an area for 477 // a single point. 478 if (getPlotArea() && item > 0 && this.pArea != null 479 && (item == (itemCount - 1) || Double.isNaN(y1))) { 480 481 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 482 plot.getRangeAxisEdge()); 483 484 // avoid possible sun.dc.pr.PRException: endPath: bad path 485 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 486 487 if (orientation == PlotOrientation.VERTICAL) { 488 // Add the last point (x,0) 489 this.pArea.addPoint((int) transX1, (int) transY2); 490 } 491 else if (orientation == PlotOrientation.HORIZONTAL) { 492 // Add the last point (x,0) 493 this.pArea.addPoint((int) transY2, (int) transX1); 494 } 495 496 // fill the polygon 497 g2.fill(this.pArea); 498 499 // draw an outline around the Area. 500 if (isOutline()) { 501 g2.setStroke(plot.getOutlineStroke()); 502 g2.setPaint(plot.getOutlinePaint()); 503 g2.draw(this.pArea); 504 } 505 506 // start new area when needed (see above) 507 this.pArea = null; 508 } 509 510 // do we need to update the crosshair values? 511 if (!Double.isNaN(y1)) { 512 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 513 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 514 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 515 rangeAxisIndex, transX1, transY1, orientation); 516 } 517 518 // collect entity and tool tip information... 519 EntityCollection entities = state.getEntityCollection(); 520 if (entities != null) { 521 addEntity(entities, shape, dataset, series, item, transX1, transY1); 522 } 523 } 524 525 /** 526 * Tests this renderer for equality with an arbitrary object. 527 * 528 * @param obj the object (<code>null</code> permitted). 529 * 530 * @return A boolean. 531 */ 532 @Override equals(Object obj)533 public boolean equals(Object obj) { 534 if (obj == this) { 535 return true; 536 } 537 if (!(obj instanceof XYStepAreaRenderer)) { 538 return false; 539 } 540 XYStepAreaRenderer that = (XYStepAreaRenderer) obj; 541 if (this.showOutline != that.showOutline) { 542 return false; 543 } 544 if (this.shapesVisible != that.shapesVisible) { 545 return false; 546 } 547 if (this.shapesFilled != that.shapesFilled) { 548 return false; 549 } 550 if (this.plotArea != that.plotArea) { 551 return false; 552 } 553 if (this.rangeBase != that.rangeBase) { 554 return false; 555 } 556 return super.equals(obj); 557 } 558 559 /** 560 * Returns a clone of the renderer. 561 * 562 * @return A clone. 563 * 564 * @throws CloneNotSupportedException if the renderer cannot be cloned. 565 */ 566 @Override clone()567 public Object clone() throws CloneNotSupportedException { 568 return super.clone(); 569 } 570 571 /** 572 * Helper method which returns a value if it lies 573 * inside the visible dataArea and otherwise the corresponding 574 * coordinate on the border of the dataArea. The PlotOrientation 575 * is taken into account. 576 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path 577 * which occurs when trying to draw lines/shapes which in large part 578 * lie outside of the visible dataArea. 579 * 580 * @param value the value which shall be 581 * @param dataArea the area within which the data is being drawn. 582 * @param plot the plot (can be used to obtain standard color 583 * information etc). 584 * @return <code>double</code> value inside the data area. 585 */ restrictValueToDataArea(double value, XYPlot plot, Rectangle2D dataArea)586 protected static double restrictValueToDataArea(double value, 587 XYPlot plot, 588 Rectangle2D dataArea) { 589 double min = 0; 590 double max = 0; 591 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 592 min = dataArea.getMinY(); 593 max = dataArea.getMaxY(); 594 } 595 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 596 min = dataArea.getMinX(); 597 max = dataArea.getMaxX(); 598 } 599 if (value < min) { 600 value = min; 601 } 602 else if (value > max) { 603 value = max; 604 } 605 return value; 606 } 607 608 } 609