001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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-2009, 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 * 035 * Changes: 036 * -------- 037 * 07-Oct-2003 : Version 1, contributed by Matthias Rose (DG); 038 * 10-Feb-2004 : Added some getter and setter methods (DG); 039 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed 040 * XYToolTipGenerator --> XYItemLabelGenerator (DG); 041 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 042 * getYValue() (DG); 043 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 044 * 06-Jul-2005 : Renamed get/setPlotShapes() --> get/setShapesVisible() (DG); 045 * ------------- JFREECHART 1.0.x --------------------------------------------- 046 * 06-Jul-2006 : Modified to call dataset methods that return double 047 * primitives only (DG); 048 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 049 * 14-Feb-2007 : Added equals() method override (DG); 050 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG); 051 * 14-May-2008 : Call addEntity() from within drawItem() (DG); 052 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG); 053 * 054 */ 055 056 package org.jfree.chart.renderer.xy; 057 058 import java.awt.Graphics2D; 059 import java.awt.Paint; 060 import java.awt.Polygon; 061 import java.awt.Shape; 062 import java.awt.Stroke; 063 import java.awt.geom.Rectangle2D; 064 import java.io.Serializable; 065 066 import org.jfree.chart.axis.ValueAxis; 067 import org.jfree.chart.entity.EntityCollection; 068 import org.jfree.chart.event.RendererChangeEvent; 069 import org.jfree.chart.labels.XYToolTipGenerator; 070 import org.jfree.chart.plot.CrosshairState; 071 import org.jfree.chart.plot.PlotOrientation; 072 import org.jfree.chart.plot.PlotRenderingInfo; 073 import org.jfree.chart.plot.XYPlot; 074 import org.jfree.chart.urls.XYURLGenerator; 075 import org.jfree.data.xy.XYDataset; 076 import org.jfree.util.PublicCloneable; 077 import org.jfree.util.ShapeUtilities; 078 079 /** 080 * A step chart renderer that fills the area between the step and the x-axis. 081 * The example shown here is generated by the 082 * <code>XYStepAreaRendererDemo1.java</code> program included in the JFreeChart 083 * demo collection: 084 * <br><br> 085 * <img src="../../../../../images/XYStepAreaRendererSample.png" 086 * alt="XYStepAreaRendererSample.png" /> 087 */ 088 public class XYStepAreaRenderer extends AbstractXYItemRenderer 089 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 090 091 /** For serialization. */ 092 private static final long serialVersionUID = -7311560779702649635L; 093 094 /** Useful constant for specifying the type of rendering (shapes only). */ 095 public static final int SHAPES = 1; 096 097 /** Useful constant for specifying the type of rendering (area only). */ 098 public static final int AREA = 2; 099 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 */ 129 public XYStepAreaRenderer() { 130 this(AREA); 131 } 132 133 /** 134 * Constructs a new renderer. 135 * 136 * @param type the type of the renderer. 137 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 public XYItemRendererState initialise(Graphics2D g2, 317 Rectangle2D dataArea, 318 XYPlot plot, 319 XYDataset data, 320 PlotRenderingInfo info) { 321 322 323 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 324 info); 325 // disable visible items optimisation - it doesn't work for this 326 // renderer... 327 state.setProcessVisibleItemsOnly(false); 328 return state; 329 330 } 331 332 333 /** 334 * Draws the visual representation of a single data item. 335 * 336 * @param g2 the graphics device. 337 * @param state the renderer state. 338 * @param dataArea the area within which the data is being drawn. 339 * @param info collects information about the drawing. 340 * @param plot the plot (can be used to obtain standard color information 341 * etc). 342 * @param domainAxis the domain axis. 343 * @param rangeAxis the range axis. 344 * @param dataset the dataset. 345 * @param series the series index (zero-based). 346 * @param item the item index (zero-based). 347 * @param crosshairState crosshair information for the plot 348 * (<code>null</code> permitted). 349 * @param pass the pass index. 350 */ 351 public void drawItem(Graphics2D g2, 352 XYItemRendererState state, 353 Rectangle2D dataArea, 354 PlotRenderingInfo info, 355 XYPlot plot, 356 ValueAxis domainAxis, 357 ValueAxis rangeAxis, 358 XYDataset dataset, 359 int series, 360 int item, 361 CrosshairState crosshairState, 362 int pass) { 363 364 PlotOrientation orientation = plot.getOrientation(); 365 366 // Get the item count for the series, so that we can know which is the 367 // end of the series. 368 int itemCount = dataset.getItemCount(series); 369 370 Paint paint = getItemPaint(series, item); 371 Stroke seriesStroke = getItemStroke(series, item); 372 g2.setPaint(paint); 373 g2.setStroke(seriesStroke); 374 375 // get the data point... 376 double x1 = dataset.getXValue(series, item); 377 double y1 = dataset.getYValue(series, item); 378 double x = x1; 379 double y = Double.isNaN(y1) ? getRangeBase() : y1; 380 double transX1 = domainAxis.valueToJava2D(x, dataArea, 381 plot.getDomainAxisEdge()); 382 double transY1 = rangeAxis.valueToJava2D(y, dataArea, 383 plot.getRangeAxisEdge()); 384 385 // avoid possible sun.dc.pr.PRException: endPath: bad path 386 transY1 = restrictValueToDataArea(transY1, plot, dataArea); 387 388 if (this.pArea == null && !Double.isNaN(y1)) { 389 390 // Create a new Area for the series 391 this.pArea = new Polygon(); 392 393 // start from Y = rangeBase 394 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 395 plot.getRangeAxisEdge()); 396 397 // avoid possible sun.dc.pr.PRException: endPath: bad path 398 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 399 400 // The first point is (x, this.baseYValue) 401 if (orientation == PlotOrientation.VERTICAL) { 402 this.pArea.addPoint((int) transX1, (int) transY2); 403 } 404 else if (orientation == PlotOrientation.HORIZONTAL) { 405 this.pArea.addPoint((int) transY2, (int) transX1); 406 } 407 } 408 409 double transX0 = 0; 410 double transY0; 411 412 double x0; 413 double y0; 414 if (item > 0) { 415 // get the previous data point... 416 x0 = dataset.getXValue(series, item - 1); 417 y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1); 418 419 x = x0; 420 y = Double.isNaN(y0) ? getRangeBase() : y0; 421 transX0 = domainAxis.valueToJava2D(x, dataArea, 422 plot.getDomainAxisEdge()); 423 transY0 = rangeAxis.valueToJava2D(y, dataArea, 424 plot.getRangeAxisEdge()); 425 426 // avoid possible sun.dc.pr.PRException: endPath: bad path 427 transY0 = restrictValueToDataArea(transY0, plot, dataArea); 428 429 if (Double.isNaN(y1)) { 430 // NULL value -> insert point on base line 431 // instead of 'step point' 432 transX1 = transX0; 433 transY0 = transY1; 434 } 435 if (transY0 != transY1) { 436 // not just a horizontal bar but need to perform a 'step'. 437 if (orientation == PlotOrientation.VERTICAL) { 438 this.pArea.addPoint((int) transX1, (int) transY0); 439 } 440 else if (orientation == PlotOrientation.HORIZONTAL) { 441 this.pArea.addPoint((int) transY0, (int) transX1); 442 } 443 } 444 } 445 446 Shape shape = null; 447 if (!Double.isNaN(y1)) { 448 // Add each point to Area (x, y) 449 if (orientation == PlotOrientation.VERTICAL) { 450 this.pArea.addPoint((int) transX1, (int) transY1); 451 } 452 else if (orientation == PlotOrientation.HORIZONTAL) { 453 this.pArea.addPoint((int) transY1, (int) transX1); 454 } 455 456 if (getShapesVisible()) { 457 shape = getItemShape(series, item); 458 if (orientation == PlotOrientation.VERTICAL) { 459 shape = ShapeUtilities.createTranslatedShape(shape, 460 transX1, transY1); 461 } 462 else if (orientation == PlotOrientation.HORIZONTAL) { 463 shape = ShapeUtilities.createTranslatedShape(shape, 464 transY1, transX1); 465 } 466 if (isShapesFilled()) { 467 g2.fill(shape); 468 } 469 else { 470 g2.draw(shape); 471 } 472 } 473 else { 474 if (orientation == PlotOrientation.VERTICAL) { 475 shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 476 4.0, 4.0); 477 } 478 else if (orientation == PlotOrientation.HORIZONTAL) { 479 shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 480 4.0, 4.0); 481 } 482 } 483 } 484 485 // Check if the item is the last item for the series or if it 486 // is a NULL value and number of items > 0. We can't draw an area for 487 // a single point. 488 if (getPlotArea() && item > 0 && this.pArea != null 489 && (item == (itemCount - 1) || Double.isNaN(y1))) { 490 491 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 492 plot.getRangeAxisEdge()); 493 494 // avoid possible sun.dc.pr.PRException: endPath: bad path 495 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 496 497 if (orientation == PlotOrientation.VERTICAL) { 498 // Add the last point (x,0) 499 this.pArea.addPoint((int) transX1, (int) transY2); 500 } 501 else if (orientation == PlotOrientation.HORIZONTAL) { 502 // Add the last point (x,0) 503 this.pArea.addPoint((int) transY2, (int) transX1); 504 } 505 506 // fill the polygon 507 g2.fill(this.pArea); 508 509 // draw an outline around the Area. 510 if (isOutline()) { 511 g2.setStroke(plot.getOutlineStroke()); 512 g2.setPaint(plot.getOutlinePaint()); 513 g2.draw(this.pArea); 514 } 515 516 // start new area when needed (see above) 517 this.pArea = null; 518 } 519 520 // do we need to update the crosshair values? 521 if (!Double.isNaN(y1)) { 522 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 523 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 524 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 525 rangeAxisIndex, transX1, transY1, orientation); 526 } 527 528 // collect entity and tool tip information... 529 EntityCollection entities = state.getEntityCollection(); 530 if (entities != null) { 531 addEntity(entities, shape, dataset, series, item, transX1, transY1); 532 } 533 } 534 535 /** 536 * Tests this renderer for equality with an arbitrary object. 537 * 538 * @param obj the object (<code>null</code> permitted). 539 * 540 * @return A boolean. 541 */ 542 public boolean equals(Object obj) { 543 if (obj == this) { 544 return true; 545 } 546 if (!(obj instanceof XYStepAreaRenderer)) { 547 return false; 548 } 549 XYStepAreaRenderer that = (XYStepAreaRenderer) obj; 550 if (this.showOutline != that.showOutline) { 551 return false; 552 } 553 if (this.shapesVisible != that.shapesVisible) { 554 return false; 555 } 556 if (this.shapesFilled != that.shapesFilled) { 557 return false; 558 } 559 if (this.plotArea != that.plotArea) { 560 return false; 561 } 562 if (this.rangeBase != that.rangeBase) { 563 return false; 564 } 565 return super.equals(obj); 566 } 567 568 /** 569 * Returns a clone of the renderer. 570 * 571 * @return A clone. 572 * 573 * @throws CloneNotSupportedException if the renderer cannot be cloned. 574 */ 575 public Object clone() throws CloneNotSupportedException { 576 return super.clone(); 577 } 578 579 /** 580 * Helper method which returns a value if it lies 581 * inside the visible dataArea and otherwise the corresponding 582 * coordinate on the border of the dataArea. The PlotOrientation 583 * is taken into account. 584 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path 585 * which occurs when trying to draw lines/shapes which in large part 586 * lie outside of the visible dataArea. 587 * 588 * @param value the value which shall be 589 * @param dataArea the area within which the data is being drawn. 590 * @param plot the plot (can be used to obtain standard color 591 * information etc). 592 * @return <code>double</code> value inside the data area. 593 */ 594 protected static double restrictValueToDataArea(double value, 595 XYPlot plot, 596 Rectangle2D dataArea) { 597 double min = 0; 598 double max = 0; 599 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 600 min = dataArea.getMinY(); 601 max = dataArea.getMaxY(); 602 } 603 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 604 min = dataArea.getMinX(); 605 max = dataArea.getMaxX(); 606 } 607 if (value < min) { 608 value = min; 609 } 610 else if (value > max) { 611 value = max; 612 } 613 return value; 614 } 615 616 }