001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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-2007, 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 * 052 */ 053 054package org.jfree.chart.renderer.xy; 055 056import java.awt.Graphics2D; 057import java.awt.Paint; 058import java.awt.Polygon; 059import java.awt.Shape; 060import java.awt.Stroke; 061import java.awt.geom.Rectangle2D; 062import java.io.Serializable; 063 064import org.jfree.chart.axis.ValueAxis; 065import org.jfree.chart.entity.EntityCollection; 066import org.jfree.chart.entity.XYItemEntity; 067import org.jfree.chart.event.RendererChangeEvent; 068import org.jfree.chart.labels.XYToolTipGenerator; 069import org.jfree.chart.plot.CrosshairState; 070import org.jfree.chart.plot.PlotOrientation; 071import org.jfree.chart.plot.PlotRenderingInfo; 072import org.jfree.chart.plot.XYPlot; 073import org.jfree.chart.urls.XYURLGenerator; 074import org.jfree.data.xy.XYDataset; 075import org.jfree.util.PublicCloneable; 076import org.jfree.util.ShapeUtilities; 077 078/** 079 * A step chart renderer that fills the area between the step and the x-axis. 080 */ 081public class XYStepAreaRenderer extends AbstractXYItemRenderer 082 implements XYItemRenderer, 083 Cloneable, 084 PublicCloneable, 085 Serializable { 086 087 /** For serialization. */ 088 private static final long serialVersionUID = -7311560779702649635L; 089 090 /** Useful constant for specifying the type of rendering (shapes only). */ 091 public static final int SHAPES = 1; 092 093 /** Useful constant for specifying the type of rendering (area only). */ 094 public static final int AREA = 2; 095 096 /** 097 * Useful constant for specifying the type of rendering (area and shapes). 098 */ 099 public static final int AREA_AND_SHAPES = 3; 100 101 /** A flag indicating whether or not shapes are drawn at each XY point. */ 102 private boolean shapesVisible; 103 104 /** A flag that controls whether or not shapes are filled for ALL series. */ 105 private boolean shapesFilled; 106 107 /** A flag indicating whether or not Area are drawn at each XY point. */ 108 private boolean plotArea; 109 110 /** A flag that controls whether or not the outline is shown. */ 111 private boolean showOutline; 112 113 /** Area of the complete series */ 114 protected transient Polygon pArea = null; 115 116 /** 117 * The value on the range axis which defines the 'lower' border of the 118 * area. 119 */ 120 private double rangeBase; 121 122 /** 123 * Constructs a new renderer. 124 */ 125 public XYStepAreaRenderer() { 126 this(AREA); 127 } 128 129 /** 130 * Constructs a new renderer. 131 * 132 * @param type the type of the renderer. 133 */ 134 public XYStepAreaRenderer(int type) { 135 this(type, null, null); 136 } 137 138 /** 139 * Constructs a new renderer. 140 * <p> 141 * To specify the type of renderer, use one of the constants: 142 * AREA, SHAPES or AREA_AND_SHAPES. 143 * 144 * @param type the type of renderer. 145 * @param toolTipGenerator the tool tip generator to use 146 * (<code>null</code> permitted). 147 * @param urlGenerator the URL generator (<code>null</code> permitted). 148 */ 149 public XYStepAreaRenderer(int type, 150 XYToolTipGenerator toolTipGenerator, 151 XYURLGenerator urlGenerator) { 152 153 super(); 154 setBaseToolTipGenerator(toolTipGenerator); 155 setURLGenerator(urlGenerator); 156 157 if (type == AREA) { 158 this.plotArea = true; 159 } 160 else if (type == SHAPES) { 161 this.shapesVisible = true; 162 } 163 else if (type == AREA_AND_SHAPES) { 164 this.plotArea = true; 165 this.shapesVisible = true; 166 } 167 this.showOutline = false; 168 } 169 170 /** 171 * Returns a flag that controls whether or not outlines of the areas are 172 * drawn. 173 * 174 * @return The flag. 175 * 176 * @see #setOutline(boolean) 177 */ 178 public boolean isOutline() { 179 return this.showOutline; 180 } 181 182 /** 183 * Sets a flag that controls whether or not outlines of the areas are 184 * drawn, and sends a {@link RendererChangeEvent} to all registered 185 * listeners. 186 * 187 * @param show the flag. 188 * 189 * @see #isOutline() 190 */ 191 public void setOutline(boolean show) { 192 this.showOutline = show; 193 notifyListeners(new RendererChangeEvent(this)); 194 } 195 196 /** 197 * Returns true if shapes are being plotted by the renderer. 198 * 199 * @return <code>true</code> if shapes are being plotted by the renderer. 200 * 201 * @see #setShapesVisible(boolean) 202 */ 203 public boolean getShapesVisible() { 204 return this.shapesVisible; 205 } 206 207 /** 208 * Sets the flag that controls whether or not shapes are displayed for each 209 * data item, and sends a {@link RendererChangeEvent} to all registered 210 * listeners. 211 * 212 * @param flag the flag. 213 * 214 * @see #getShapesVisible() 215 */ 216 public void setShapesVisible(boolean flag) { 217 this.shapesVisible = flag; 218 notifyListeners(new RendererChangeEvent(this)); 219 } 220 221 /** 222 * Returns the flag that controls whether or not the shapes are filled. 223 * 224 * @return A boolean. 225 * 226 * @see #setShapesFilled(boolean) 227 */ 228 public boolean isShapesFilled() { 229 return this.shapesFilled; 230 } 231 232 /** 233 * Sets the 'shapes filled' for ALL series. 234 * 235 * @param filled the flag. 236 * 237 * @see #isShapesFilled() 238 */ 239 public void setShapesFilled(boolean filled) { 240 this.shapesFilled = filled; 241 notifyListeners(new RendererChangeEvent(this)); 242 } 243 244 /** 245 * Returns true if Area is being plotted by the renderer. 246 * 247 * @return <code>true</code> if Area is being plotted by the renderer. 248 * 249 * @see #setPlotArea(boolean) 250 */ 251 public boolean getPlotArea() { 252 return this.plotArea; 253 } 254 255 /** 256 * Sets a flag that controls whether or not areas are drawn for each data 257 * item. 258 * 259 * @param flag the flag. 260 * 261 * @see #getPlotArea() 262 */ 263 public void setPlotArea(boolean flag) { 264 this.plotArea = flag; 265 notifyListeners(new RendererChangeEvent(this)); 266 } 267 268 /** 269 * Returns the value on the range axis which defines the 'lower' border of 270 * the area. 271 * 272 * @return <code>double</code> the value on the range axis which defines 273 * the 'lower' border of the area. 274 * 275 * @see #setRangeBase(double) 276 */ 277 public double getRangeBase() { 278 return this.rangeBase; 279 } 280 281 /** 282 * Sets the value on the range axis which defines the default border of the 283 * area. 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 notifyListeners(new RendererChangeEvent(this)); 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 if (state.getInfo() != null) { 524 EntityCollection entities = state.getEntityCollection(); 525 if (entities != null && shape != null) { 526 String tip = null; 527 XYToolTipGenerator generator 528 = getToolTipGenerator(series, item); 529 if (generator != null) { 530 tip = generator.generateToolTip(dataset, series, item); 531 } 532 String url = null; 533 if (getURLGenerator() != null) { 534 url = getURLGenerator().generateURL(dataset, series, item); 535 } 536 XYItemEntity entity = new XYItemEntity(shape, dataset, series, 537 item, tip, url); 538 entities.add(entity); 539 } 540 } 541 } 542 543 /** 544 * Tests this renderer for equality with an arbitrary object. 545 * 546 * @param obj the object (<code>null</code> permitted). 547 * 548 * @return A boolean. 549 */ 550 public boolean equals(Object obj) { 551 if (obj == this) { 552 return true; 553 } 554 if (!(obj instanceof XYStepAreaRenderer)) { 555 return false; 556 } 557 XYStepAreaRenderer that = (XYStepAreaRenderer) obj; 558 if (this.showOutline != that.showOutline) { 559 return false; 560 } 561 if (this.shapesVisible != that.shapesVisible) { 562 return false; 563 } 564 if (this.shapesFilled != that.shapesFilled) { 565 return false; 566 } 567 if (this.plotArea != that.plotArea) { 568 return false; 569 } 570 if (this.rangeBase != that.rangeBase) { 571 return false; 572 } 573 return super.equals(obj); 574 } 575 576 /** 577 * Returns a clone of the renderer. 578 * 579 * @return A clone. 580 * 581 * @throws CloneNotSupportedException if the renderer cannot be cloned. 582 */ 583 public Object clone() throws CloneNotSupportedException { 584 return super.clone(); 585 } 586 587 /** 588 * Helper method which returns a value if it lies 589 * inside the visible dataArea and otherwise the corresponding 590 * coordinate on the border of the dataArea. The PlotOrientation 591 * is taken into account. 592 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path 593 * which occurs when trying to draw lines/shapes which in large part 594 * lie outside of the visible dataArea. 595 * 596 * @param value the value which shall be 597 * @param dataArea the area within which the data is being drawn. 598 * @param plot the plot (can be used to obtain standard color 599 * information etc). 600 * @return <code>double</code> value inside the data area. 601 */ 602 protected static double restrictValueToDataArea(double value, 603 XYPlot plot, 604 Rectangle2D dataArea) { 605 double min = 0; 606 double max = 0; 607 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 608 min = dataArea.getMinY(); 609 max = dataArea.getMaxY(); 610 } 611 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 612 min = dataArea.getMinX(); 613 max = dataArea.getMaxX(); 614 } 615 if (value < min) { 616 value = min; 617 } 618 else if (value > max) { 619 value = max; 620 } 621 return value; 622 } 623 624}