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