001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ----------------------- 028 * XYStepAreaRenderer.java 029 * ----------------------- 030 * (C) Copyright 2003-2008, 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 * 053 */ 054 055 package org.jfree.chart.renderer.xy; 056 057 import java.awt.Graphics2D; 058 import java.awt.Paint; 059 import java.awt.Polygon; 060 import java.awt.Shape; 061 import java.awt.Stroke; 062 import java.awt.geom.Rectangle2D; 063 import java.io.Serializable; 064 065 import org.jfree.chart.axis.ValueAxis; 066 import org.jfree.chart.entity.EntityCollection; 067 import org.jfree.chart.event.RendererChangeEvent; 068 import org.jfree.chart.labels.XYToolTipGenerator; 069 import org.jfree.chart.plot.CrosshairState; 070 import org.jfree.chart.plot.PlotOrientation; 071 import org.jfree.chart.plot.PlotRenderingInfo; 072 import org.jfree.chart.plot.XYPlot; 073 import org.jfree.chart.urls.XYURLGenerator; 074 import org.jfree.data.xy.XYDataset; 075 import org.jfree.util.PublicCloneable; 076 import org.jfree.util.ShapeUtilities; 077 078 /** 079 * A step chart renderer that fills the area between the step and the x-axis. 080 * The example shown here is generated by the 081 * <code>XYStepAreaRendererDemo1.java</code> program included in the JFreeChart 082 * demo collection: 083 * <br><br> 084 * <img src="../../../../../images/XYStepAreaRendererSample.png" 085 * alt="XYStepAreaRendererSample.png" /> 086 */ 087 public class XYStepAreaRenderer extends AbstractXYItemRenderer 088 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 089 090 /** For serialization. */ 091 private static final long serialVersionUID = -7311560779702649635L; 092 093 /** Useful constant for specifying the type of rendering (shapes only). */ 094 public static final int SHAPES = 1; 095 096 /** Useful constant for specifying the type of rendering (area only). */ 097 public static final int AREA = 2; 098 099 /** 100 * Useful constant for specifying the type of rendering (area and shapes). 101 */ 102 public static final int AREA_AND_SHAPES = 3; 103 104 /** A flag indicating whether or not shapes are drawn at each XY point. */ 105 private boolean shapesVisible; 106 107 /** A flag that controls whether or not shapes are filled for ALL series. */ 108 private boolean shapesFilled; 109 110 /** A flag indicating whether or not Area are drawn at each XY point. */ 111 private boolean plotArea; 112 113 /** A flag that controls whether or not the outline is shown. */ 114 private boolean showOutline; 115 116 /** Area of the complete series */ 117 protected transient Polygon pArea = null; 118 119 /** 120 * The value on the range axis which defines the 'lower' border of the 121 * area. 122 */ 123 private double rangeBase; 124 125 /** 126 * Constructs a new renderer. 127 */ 128 public XYStepAreaRenderer() { 129 this(AREA); 130 } 131 132 /** 133 * Constructs a new renderer. 134 * 135 * @param type the type of the renderer. 136 */ 137 public XYStepAreaRenderer(int type) { 138 this(type, null, null); 139 } 140 141 /** 142 * Constructs a new renderer. 143 * <p> 144 * To specify the type of renderer, use one of the constants: 145 * AREA, SHAPES or AREA_AND_SHAPES. 146 * 147 * @param type the type of renderer. 148 * @param toolTipGenerator the tool tip generator to use 149 * (<code>null</code> permitted). 150 * @param urlGenerator the URL generator (<code>null</code> permitted). 151 */ 152 public XYStepAreaRenderer(int type, 153 XYToolTipGenerator toolTipGenerator, 154 XYURLGenerator urlGenerator) { 155 156 super(); 157 setBaseToolTipGenerator(toolTipGenerator); 158 setURLGenerator(urlGenerator); 159 160 if (type == AREA) { 161 this.plotArea = true; 162 } 163 else if (type == SHAPES) { 164 this.shapesVisible = true; 165 } 166 else if (type == AREA_AND_SHAPES) { 167 this.plotArea = true; 168 this.shapesVisible = true; 169 } 170 this.showOutline = false; 171 } 172 173 /** 174 * Returns a flag that controls whether or not outlines of the areas are 175 * drawn. 176 * 177 * @return The flag. 178 * 179 * @see #setOutline(boolean) 180 */ 181 public boolean isOutline() { 182 return this.showOutline; 183 } 184 185 /** 186 * Sets a flag that controls whether or not outlines of the areas are 187 * drawn, and sends a {@link RendererChangeEvent} to all registered 188 * listeners. 189 * 190 * @param show the flag. 191 * 192 * @see #isOutline() 193 */ 194 public void setOutline(boolean show) { 195 this.showOutline = show; 196 fireChangeEvent(); 197 } 198 199 /** 200 * Returns true if shapes are being plotted by the renderer. 201 * 202 * @return <code>true</code> if shapes are being plotted by the renderer. 203 * 204 * @see #setShapesVisible(boolean) 205 */ 206 public boolean getShapesVisible() { 207 return this.shapesVisible; 208 } 209 210 /** 211 * Sets the flag that controls whether or not shapes are displayed for each 212 * data item, and sends a {@link RendererChangeEvent} to all registered 213 * listeners. 214 * 215 * @param flag the flag. 216 * 217 * @see #getShapesVisible() 218 */ 219 public void setShapesVisible(boolean flag) { 220 this.shapesVisible = flag; 221 fireChangeEvent(); 222 } 223 224 /** 225 * Returns the flag that controls whether or not the shapes are filled. 226 * 227 * @return A boolean. 228 * 229 * @see #setShapesFilled(boolean) 230 */ 231 public boolean isShapesFilled() { 232 return this.shapesFilled; 233 } 234 235 /** 236 * Sets the 'shapes filled' for ALL series and sends a 237 * {@link RendererChangeEvent} to all registered listeners. 238 * 239 * @param filled the flag. 240 * 241 * @see #isShapesFilled() 242 */ 243 public void setShapesFilled(boolean filled) { 244 this.shapesFilled = filled; 245 fireChangeEvent(); 246 } 247 248 /** 249 * Returns true if Area is being plotted by the renderer. 250 * 251 * @return <code>true</code> if Area is being plotted by the renderer. 252 * 253 * @see #setPlotArea(boolean) 254 */ 255 public boolean getPlotArea() { 256 return this.plotArea; 257 } 258 259 /** 260 * Sets a flag that controls whether or not areas are drawn for each data 261 * item and sends a {@link RendererChangeEvent} to all registered 262 * listeners. 263 * 264 * @param flag the flag. 265 * 266 * @see #getPlotArea() 267 */ 268 public void setPlotArea(boolean flag) { 269 this.plotArea = flag; 270 fireChangeEvent(); 271 } 272 273 /** 274 * Returns the value on the range axis which defines the 'lower' border of 275 * the area. 276 * 277 * @return <code>double</code> the value on the range axis which defines 278 * the 'lower' border of the area. 279 * 280 * @see #setRangeBase(double) 281 */ 282 public double getRangeBase() { 283 return this.rangeBase; 284 } 285 286 /** 287 * Sets the value on the range axis which defines the default border of the 288 * area, and sends a {@link RendererChangeEvent} to all registered 289 * listeners. E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 290 * reach the lower border of the plotArea. 291 * 292 * @param val the value on the range axis which defines the default border 293 * of the area. 294 * 295 * @see #getRangeBase() 296 */ 297 public void setRangeBase(double val) { 298 this.rangeBase = val; 299 fireChangeEvent(); 300 } 301 302 /** 303 * Initialises the renderer. Here we calculate the Java2D y-coordinate for 304 * zero, since all the bars have their bases fixed at zero. 305 * 306 * @param g2 the graphics device. 307 * @param dataArea the area inside the axes. 308 * @param plot the plot. 309 * @param data the data. 310 * @param info an optional info collection object to return data back to 311 * the caller. 312 * 313 * @return The number of passes required by the renderer. 314 */ 315 public XYItemRendererState initialise(Graphics2D g2, 316 Rectangle2D dataArea, 317 XYPlot plot, 318 XYDataset data, 319 PlotRenderingInfo info) { 320 321 322 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 323 info); 324 // disable visible items optimisation - it doesn't work for this 325 // renderer... 326 state.setProcessVisibleItemsOnly(false); 327 return state; 328 329 } 330 331 332 /** 333 * Draws the visual representation of a single data item. 334 * 335 * @param g2 the graphics device. 336 * @param state the renderer state. 337 * @param dataArea the area within which the data is being drawn. 338 * @param info collects information about the drawing. 339 * @param plot the plot (can be used to obtain standard color information 340 * etc). 341 * @param domainAxis the domain axis. 342 * @param rangeAxis the range axis. 343 * @param dataset the dataset. 344 * @param series the series index (zero-based). 345 * @param item the item index (zero-based). 346 * @param crosshairState crosshair information for the plot 347 * (<code>null</code> permitted). 348 * @param pass the pass index. 349 */ 350 public void drawItem(Graphics2D g2, 351 XYItemRendererState state, 352 Rectangle2D dataArea, 353 PlotRenderingInfo info, 354 XYPlot plot, 355 ValueAxis domainAxis, 356 ValueAxis rangeAxis, 357 XYDataset dataset, 358 int series, 359 int item, 360 CrosshairState crosshairState, 361 int pass) { 362 363 PlotOrientation orientation = plot.getOrientation(); 364 365 // Get the item count for the series, so that we can know which is the 366 // end of the series. 367 int itemCount = dataset.getItemCount(series); 368 369 Paint paint = getItemPaint(series, item); 370 Stroke seriesStroke = getItemStroke(series, item); 371 g2.setPaint(paint); 372 g2.setStroke(seriesStroke); 373 374 // get the data point... 375 double x1 = dataset.getXValue(series, item); 376 double y1 = dataset.getYValue(series, item); 377 double x = x1; 378 double y = Double.isNaN(y1) ? getRangeBase() : y1; 379 double transX1 = domainAxis.valueToJava2D(x, dataArea, 380 plot.getDomainAxisEdge()); 381 double transY1 = rangeAxis.valueToJava2D(y, dataArea, 382 plot.getRangeAxisEdge()); 383 384 // avoid possible sun.dc.pr.PRException: endPath: bad path 385 transY1 = restrictValueToDataArea(transY1, plot, dataArea); 386 387 if (this.pArea == null && !Double.isNaN(y1)) { 388 389 // Create a new Area for the series 390 this.pArea = new Polygon(); 391 392 // start from Y = rangeBase 393 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 394 plot.getRangeAxisEdge()); 395 396 // avoid possible sun.dc.pr.PRException: endPath: bad path 397 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 398 399 // The first point is (x, this.baseYValue) 400 if (orientation == PlotOrientation.VERTICAL) { 401 this.pArea.addPoint((int) transX1, (int) transY2); 402 } 403 else if (orientation == PlotOrientation.HORIZONTAL) { 404 this.pArea.addPoint((int) transY2, (int) transX1); 405 } 406 } 407 408 double transX0 = 0; 409 double transY0 = restrictValueToDataArea(getRangeBase(), plot, 410 dataArea); 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 }