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 */ 081 public class XYStepAreaRenderer extends AbstractXYItemRenderer 082 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 083 084 /** For serialization. */ 085 private static final long serialVersionUID = -7311560779702649635L; 086 087 /** Useful constant for specifying the type of rendering (shapes only). */ 088 public static final int SHAPES = 1; 089 090 /** Useful constant for specifying the type of rendering (area only). */ 091 public static final int AREA = 2; 092 093 /** 094 * Useful constant for specifying the type of rendering (area and shapes). 095 */ 096 public static final int AREA_AND_SHAPES = 3; 097 098 /** A flag indicating whether or not shapes are drawn at each XY point. */ 099 private boolean shapesVisible; 100 101 /** A flag that controls whether or not shapes are filled for ALL series. */ 102 private boolean shapesFilled; 103 104 /** A flag indicating whether or not Area are drawn at each XY point. */ 105 private boolean plotArea; 106 107 /** A flag that controls whether or not the outline is shown. */ 108 private boolean showOutline; 109 110 /** Area of the complete series */ 111 protected transient Polygon pArea = null; 112 113 /** 114 * The value on the range axis which defines the 'lower' border of the 115 * area. 116 */ 117 private double rangeBase; 118 119 /** 120 * Constructs a new renderer. 121 */ 122 public XYStepAreaRenderer() { 123 this(AREA); 124 } 125 126 /** 127 * Constructs a new renderer. 128 * 129 * @param type the type of the renderer. 130 */ 131 public XYStepAreaRenderer(int type) { 132 this(type, null, null); 133 } 134 135 /** 136 * Constructs a new renderer. 137 * <p> 138 * To specify the type of renderer, use one of the constants: 139 * AREA, SHAPES or AREA_AND_SHAPES. 140 * 141 * @param type the type of renderer. 142 * @param toolTipGenerator the tool tip generator to use 143 * (<code>null</code> permitted). 144 * @param urlGenerator the URL generator (<code>null</code> permitted). 145 */ 146 public XYStepAreaRenderer(int type, 147 XYToolTipGenerator toolTipGenerator, 148 XYURLGenerator urlGenerator) { 149 150 super(); 151 setBaseToolTipGenerator(toolTipGenerator); 152 setURLGenerator(urlGenerator); 153 154 if (type == AREA) { 155 this.plotArea = true; 156 } 157 else if (type == SHAPES) { 158 this.shapesVisible = true; 159 } 160 else if (type == AREA_AND_SHAPES) { 161 this.plotArea = true; 162 this.shapesVisible = true; 163 } 164 this.showOutline = false; 165 } 166 167 /** 168 * Returns a flag that controls whether or not outlines of the areas are 169 * drawn. 170 * 171 * @return The flag. 172 * 173 * @see #setOutline(boolean) 174 */ 175 public boolean isOutline() { 176 return this.showOutline; 177 } 178 179 /** 180 * Sets a flag that controls whether or not outlines of the areas are 181 * drawn, and sends a {@link RendererChangeEvent} to all registered 182 * listeners. 183 * 184 * @param show the flag. 185 * 186 * @see #isOutline() 187 */ 188 public void setOutline(boolean show) { 189 this.showOutline = show; 190 fireChangeEvent(); 191 } 192 193 /** 194 * Returns true if shapes are being plotted by the renderer. 195 * 196 * @return <code>true</code> if shapes are being plotted by the renderer. 197 * 198 * @see #setShapesVisible(boolean) 199 */ 200 public boolean getShapesVisible() { 201 return this.shapesVisible; 202 } 203 204 /** 205 * Sets the flag that controls whether or not shapes are displayed for each 206 * data item, and sends a {@link RendererChangeEvent} to all registered 207 * listeners. 208 * 209 * @param flag the flag. 210 * 211 * @see #getShapesVisible() 212 */ 213 public void setShapesVisible(boolean flag) { 214 this.shapesVisible = flag; 215 fireChangeEvent(); 216 } 217 218 /** 219 * Returns the flag that controls whether or not the shapes are filled. 220 * 221 * @return A boolean. 222 * 223 * @see #setShapesFilled(boolean) 224 */ 225 public boolean isShapesFilled() { 226 return this.shapesFilled; 227 } 228 229 /** 230 * Sets the 'shapes filled' for ALL series and sends a 231 * {@link RendererChangeEvent} to all registered listeners. 232 * 233 * @param filled the flag. 234 * 235 * @see #isShapesFilled() 236 */ 237 public void setShapesFilled(boolean filled) { 238 this.shapesFilled = filled; 239 fireChangeEvent(); 240 } 241 242 /** 243 * Returns true if Area is being plotted by the renderer. 244 * 245 * @return <code>true</code> if Area is being plotted by the renderer. 246 * 247 * @see #setPlotArea(boolean) 248 */ 249 public boolean getPlotArea() { 250 return this.plotArea; 251 } 252 253 /** 254 * Sets a flag that controls whether or not areas are drawn for each data 255 * item and sends a {@link RendererChangeEvent} to all registered 256 * listeners. 257 * 258 * @param flag the flag. 259 * 260 * @see #getPlotArea() 261 */ 262 public void setPlotArea(boolean flag) { 263 this.plotArea = flag; 264 fireChangeEvent(); 265 } 266 267 /** 268 * Returns the value on the range axis which defines the 'lower' border of 269 * the area. 270 * 271 * @return <code>double</code> the value on the range axis which defines 272 * the 'lower' border of the area. 273 * 274 * @see #setRangeBase(double) 275 */ 276 public double getRangeBase() { 277 return this.rangeBase; 278 } 279 280 /** 281 * Sets the value on the range axis which defines the default border of the 282 * area, and sends a {@link RendererChangeEvent} to all registered 283 * listeners. E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 284 * reach the lower border of the plotArea. 285 * 286 * @param val the value on the range axis which defines the default border 287 * of the area. 288 * 289 * @see #getRangeBase() 290 */ 291 public void setRangeBase(double val) { 292 this.rangeBase = val; 293 fireChangeEvent(); 294 } 295 296 /** 297 * Initialises the renderer. Here we calculate the Java2D y-coordinate for 298 * zero, since all the bars have their bases fixed at zero. 299 * 300 * @param g2 the graphics device. 301 * @param dataArea the area inside the axes. 302 * @param plot the plot. 303 * @param data the data. 304 * @param info an optional info collection object to return data back to 305 * the caller. 306 * 307 * @return The number of passes required by the renderer. 308 */ 309 public XYItemRendererState initialise(Graphics2D g2, 310 Rectangle2D dataArea, 311 XYPlot plot, 312 XYDataset data, 313 PlotRenderingInfo info) { 314 315 316 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 317 info); 318 // disable visible items optimisation - it doesn't work for this 319 // renderer... 320 state.setProcessVisibleItemsOnly(false); 321 return state; 322 323 } 324 325 326 /** 327 * Draws the visual representation of a single data item. 328 * 329 * @param g2 the graphics device. 330 * @param state the renderer state. 331 * @param dataArea the area within which the data is being drawn. 332 * @param info collects information about the drawing. 333 * @param plot the plot (can be used to obtain standard color information 334 * etc). 335 * @param domainAxis the domain axis. 336 * @param rangeAxis the range axis. 337 * @param dataset the dataset. 338 * @param series the series index (zero-based). 339 * @param item the item index (zero-based). 340 * @param crosshairState crosshair information for the plot 341 * (<code>null</code> permitted). 342 * @param pass the pass index. 343 */ 344 public void drawItem(Graphics2D g2, 345 XYItemRendererState state, 346 Rectangle2D dataArea, 347 PlotRenderingInfo info, 348 XYPlot plot, 349 ValueAxis domainAxis, 350 ValueAxis rangeAxis, 351 XYDataset dataset, 352 int series, 353 int item, 354 CrosshairState crosshairState, 355 int pass) { 356 357 PlotOrientation orientation = plot.getOrientation(); 358 359 // Get the item count for the series, so that we can know which is the 360 // end of the series. 361 int itemCount = dataset.getItemCount(series); 362 363 Paint paint = getItemPaint(series, item); 364 Stroke seriesStroke = getItemStroke(series, item); 365 g2.setPaint(paint); 366 g2.setStroke(seriesStroke); 367 368 // get the data point... 369 double x1 = dataset.getXValue(series, item); 370 double y1 = dataset.getYValue(series, item); 371 double x = x1; 372 double y = Double.isNaN(y1) ? getRangeBase() : y1; 373 double transX1 = domainAxis.valueToJava2D(x, dataArea, 374 plot.getDomainAxisEdge()); 375 double transY1 = rangeAxis.valueToJava2D(y, dataArea, 376 plot.getRangeAxisEdge()); 377 378 // avoid possible sun.dc.pr.PRException: endPath: bad path 379 transY1 = restrictValueToDataArea(transY1, plot, dataArea); 380 381 if (this.pArea == null && !Double.isNaN(y1)) { 382 383 // Create a new Area for the series 384 this.pArea = new Polygon(); 385 386 // start from Y = rangeBase 387 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 388 plot.getRangeAxisEdge()); 389 390 // avoid possible sun.dc.pr.PRException: endPath: bad path 391 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 392 393 // The first point is (x, this.baseYValue) 394 if (orientation == PlotOrientation.VERTICAL) { 395 this.pArea.addPoint((int) transX1, (int) transY2); 396 } 397 else if (orientation == PlotOrientation.HORIZONTAL) { 398 this.pArea.addPoint((int) transY2, (int) transX1); 399 } 400 } 401 402 double transX0 = 0; 403 double transY0 = restrictValueToDataArea(getRangeBase(), plot, 404 dataArea); 405 406 double x0; 407 double y0; 408 if (item > 0) { 409 // get the previous data point... 410 x0 = dataset.getXValue(series, item - 1); 411 y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1); 412 413 x = x0; 414 y = Double.isNaN(y0) ? getRangeBase() : y0; 415 transX0 = domainAxis.valueToJava2D(x, dataArea, 416 plot.getDomainAxisEdge()); 417 transY0 = rangeAxis.valueToJava2D(y, dataArea, 418 plot.getRangeAxisEdge()); 419 420 // avoid possible sun.dc.pr.PRException: endPath: bad path 421 transY0 = restrictValueToDataArea(transY0, plot, dataArea); 422 423 if (Double.isNaN(y1)) { 424 // NULL value -> insert point on base line 425 // instead of 'step point' 426 transX1 = transX0; 427 transY0 = transY1; 428 } 429 if (transY0 != transY1) { 430 // not just a horizontal bar but need to perform a 'step'. 431 if (orientation == PlotOrientation.VERTICAL) { 432 this.pArea.addPoint((int) transX1, (int) transY0); 433 } 434 else if (orientation == PlotOrientation.HORIZONTAL) { 435 this.pArea.addPoint((int) transY0, (int) transX1); 436 } 437 } 438 } 439 440 Shape shape = null; 441 if (!Double.isNaN(y1)) { 442 // Add each point to Area (x, y) 443 if (orientation == PlotOrientation.VERTICAL) { 444 this.pArea.addPoint((int) transX1, (int) transY1); 445 } 446 else if (orientation == PlotOrientation.HORIZONTAL) { 447 this.pArea.addPoint((int) transY1, (int) transX1); 448 } 449 450 if (getShapesVisible()) { 451 shape = getItemShape(series, item); 452 if (orientation == PlotOrientation.VERTICAL) { 453 shape = ShapeUtilities.createTranslatedShape(shape, 454 transX1, transY1); 455 } 456 else if (orientation == PlotOrientation.HORIZONTAL) { 457 shape = ShapeUtilities.createTranslatedShape(shape, 458 transY1, transX1); 459 } 460 if (isShapesFilled()) { 461 g2.fill(shape); 462 } 463 else { 464 g2.draw(shape); 465 } 466 } 467 else { 468 if (orientation == PlotOrientation.VERTICAL) { 469 shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 470 4.0, 4.0); 471 } 472 else if (orientation == PlotOrientation.HORIZONTAL) { 473 shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 474 4.0, 4.0); 475 } 476 } 477 } 478 479 // Check if the item is the last item for the series or if it 480 // is a NULL value and number of items > 0. We can't draw an area for 481 // a single point. 482 if (getPlotArea() && item > 0 && this.pArea != null 483 && (item == (itemCount - 1) || Double.isNaN(y1))) { 484 485 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 486 plot.getRangeAxisEdge()); 487 488 // avoid possible sun.dc.pr.PRException: endPath: bad path 489 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 490 491 if (orientation == PlotOrientation.VERTICAL) { 492 // Add the last point (x,0) 493 this.pArea.addPoint((int) transX1, (int) transY2); 494 } 495 else if (orientation == PlotOrientation.HORIZONTAL) { 496 // Add the last point (x,0) 497 this.pArea.addPoint((int) transY2, (int) transX1); 498 } 499 500 // fill the polygon 501 g2.fill(this.pArea); 502 503 // draw an outline around the Area. 504 if (isOutline()) { 505 g2.setStroke(plot.getOutlineStroke()); 506 g2.setPaint(plot.getOutlinePaint()); 507 g2.draw(this.pArea); 508 } 509 510 // start new area when needed (see above) 511 this.pArea = null; 512 } 513 514 // do we need to update the crosshair values? 515 if (!Double.isNaN(y1)) { 516 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 517 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 518 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 519 rangeAxisIndex, transX1, transY1, orientation); 520 } 521 522 // collect entity and tool tip information... 523 EntityCollection entities = state.getEntityCollection(); 524 if (entities != null) { 525 addEntity(entities, shape, dataset, series, item, transX1, transY1); 526 } 527 } 528 529 /** 530 * Tests this renderer for equality with an arbitrary object. 531 * 532 * @param obj the object (<code>null</code> permitted). 533 * 534 * @return A boolean. 535 */ 536 public boolean equals(Object obj) { 537 if (obj == this) { 538 return true; 539 } 540 if (!(obj instanceof XYStepAreaRenderer)) { 541 return false; 542 } 543 XYStepAreaRenderer that = (XYStepAreaRenderer) obj; 544 if (this.showOutline != that.showOutline) { 545 return false; 546 } 547 if (this.shapesVisible != that.shapesVisible) { 548 return false; 549 } 550 if (this.shapesFilled != that.shapesFilled) { 551 return false; 552 } 553 if (this.plotArea != that.plotArea) { 554 return false; 555 } 556 if (this.rangeBase != that.rangeBase) { 557 return false; 558 } 559 return super.equals(obj); 560 } 561 562 /** 563 * Returns a clone of the renderer. 564 * 565 * @return A clone. 566 * 567 * @throws CloneNotSupportedException if the renderer cannot be cloned. 568 */ 569 public Object clone() throws CloneNotSupportedException { 570 return super.clone(); 571 } 572 573 /** 574 * Helper method which returns a value if it lies 575 * inside the visible dataArea and otherwise the corresponding 576 * coordinate on the border of the dataArea. The PlotOrientation 577 * is taken into account. 578 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path 579 * which occurs when trying to draw lines/shapes which in large part 580 * lie outside of the visible dataArea. 581 * 582 * @param value the value which shall be 583 * @param dataArea the area within which the data is being drawn. 584 * @param plot the plot (can be used to obtain standard color 585 * information etc). 586 * @return <code>double</code> value inside the data area. 587 */ 588 protected static double restrictValueToDataArea(double value, 589 XYPlot plot, 590 Rectangle2D dataArea) { 591 double min = 0; 592 double max = 0; 593 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 594 min = dataArea.getMinY(); 595 max = dataArea.getMaxY(); 596 } 597 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 598 min = dataArea.getMinX(); 599 max = dataArea.getMaxX(); 600 } 601 if (value < min) { 602 value = min; 603 } 604 else if (value > max) { 605 value = max; 606 } 607 return value; 608 } 609 610 }