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 054 package org.jfree.chart.renderer.xy; 055 056 import java.awt.Graphics2D; 057 import java.awt.Paint; 058 import java.awt.Polygon; 059 import java.awt.Shape; 060 import java.awt.Stroke; 061 import java.awt.geom.Rectangle2D; 062 import java.io.Serializable; 063 064 import org.jfree.chart.axis.ValueAxis; 065 import org.jfree.chart.entity.EntityCollection; 066 import org.jfree.chart.entity.XYItemEntity; 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, 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 fireChangeEvent(); 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 fireChangeEvent(); 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 and sends a 234 * {@link RendererChangeEvent} to all registered listeners. 235 * 236 * @param filled the flag. 237 * 238 * @see #isShapesFilled() 239 */ 240 public void setShapesFilled(boolean filled) { 241 this.shapesFilled = filled; 242 fireChangeEvent(); 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 and sends a {@link RendererChangeEvent} to all registered 259 * listeners. 260 * 261 * @param flag the flag. 262 * 263 * @see #getPlotArea() 264 */ 265 public void setPlotArea(boolean flag) { 266 this.plotArea = flag; 267 fireChangeEvent(); 268 } 269 270 /** 271 * Returns the value on the range axis which defines the 'lower' border of 272 * the area. 273 * 274 * @return <code>double</code> the value on the range axis which defines 275 * the 'lower' border of the area. 276 * 277 * @see #setRangeBase(double) 278 */ 279 public double getRangeBase() { 280 return this.rangeBase; 281 } 282 283 /** 284 * Sets the value on the range axis which defines the default border of the 285 * area, and sends a {@link RendererChangeEvent} to all registered 286 * listeners. E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 287 * reach the lower border of the plotArea. 288 * 289 * @param val the value on the range axis which defines the default border 290 * of the area. 291 * 292 * @see #getRangeBase() 293 */ 294 public void setRangeBase(double val) { 295 this.rangeBase = val; 296 fireChangeEvent(); 297 } 298 299 /** 300 * Initialises the renderer. Here we calculate the Java2D y-coordinate for 301 * zero, since all the bars have their bases fixed at zero. 302 * 303 * @param g2 the graphics device. 304 * @param dataArea the area inside the axes. 305 * @param plot the plot. 306 * @param data the data. 307 * @param info an optional info collection object to return data back to 308 * the caller. 309 * 310 * @return The number of passes required by the renderer. 311 */ 312 public XYItemRendererState initialise(Graphics2D g2, 313 Rectangle2D dataArea, 314 XYPlot plot, 315 XYDataset data, 316 PlotRenderingInfo info) { 317 318 319 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 320 info); 321 // disable visible items optimisation - it doesn't work for this 322 // renderer... 323 state.setProcessVisibleItemsOnly(false); 324 return state; 325 326 } 327 328 329 /** 330 * Draws the visual representation of a single data item. 331 * 332 * @param g2 the graphics device. 333 * @param state the renderer state. 334 * @param dataArea the area within which the data is being drawn. 335 * @param info collects information about the drawing. 336 * @param plot the plot (can be used to obtain standard color information 337 * etc). 338 * @param domainAxis the domain axis. 339 * @param rangeAxis the range axis. 340 * @param dataset the dataset. 341 * @param series the series index (zero-based). 342 * @param item the item index (zero-based). 343 * @param crosshairState crosshair information for the plot 344 * (<code>null</code> permitted). 345 * @param pass the pass index. 346 */ 347 public void drawItem(Graphics2D g2, 348 XYItemRendererState state, 349 Rectangle2D dataArea, 350 PlotRenderingInfo info, 351 XYPlot plot, 352 ValueAxis domainAxis, 353 ValueAxis rangeAxis, 354 XYDataset dataset, 355 int series, 356 int item, 357 CrosshairState crosshairState, 358 int pass) { 359 360 PlotOrientation orientation = plot.getOrientation(); 361 362 // Get the item count for the series, so that we can know which is the 363 // end of the series. 364 int itemCount = dataset.getItemCount(series); 365 366 Paint paint = getItemPaint(series, item); 367 Stroke seriesStroke = getItemStroke(series, item); 368 g2.setPaint(paint); 369 g2.setStroke(seriesStroke); 370 371 // get the data point... 372 double x1 = dataset.getXValue(series, item); 373 double y1 = dataset.getYValue(series, item); 374 double x = x1; 375 double y = Double.isNaN(y1) ? getRangeBase() : y1; 376 double transX1 = domainAxis.valueToJava2D(x, dataArea, 377 plot.getDomainAxisEdge()); 378 double transY1 = rangeAxis.valueToJava2D(y, dataArea, 379 plot.getRangeAxisEdge()); 380 381 // avoid possible sun.dc.pr.PRException: endPath: bad path 382 transY1 = restrictValueToDataArea(transY1, plot, dataArea); 383 384 if (this.pArea == null && !Double.isNaN(y1)) { 385 386 // Create a new Area for the series 387 this.pArea = new Polygon(); 388 389 // start from Y = rangeBase 390 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 391 plot.getRangeAxisEdge()); 392 393 // avoid possible sun.dc.pr.PRException: endPath: bad path 394 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 395 396 // The first point is (x, this.baseYValue) 397 if (orientation == PlotOrientation.VERTICAL) { 398 this.pArea.addPoint((int) transX1, (int) transY2); 399 } 400 else if (orientation == PlotOrientation.HORIZONTAL) { 401 this.pArea.addPoint((int) transY2, (int) transX1); 402 } 403 } 404 405 double transX0 = 0; 406 double transY0 = restrictValueToDataArea(getRangeBase(), plot, 407 dataArea); 408 409 double x0; 410 double y0; 411 if (item > 0) { 412 // get the previous data point... 413 x0 = dataset.getXValue(series, item - 1); 414 y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1); 415 416 x = x0; 417 y = Double.isNaN(y0) ? getRangeBase() : y0; 418 transX0 = domainAxis.valueToJava2D(x, dataArea, 419 plot.getDomainAxisEdge()); 420 transY0 = rangeAxis.valueToJava2D(y, dataArea, 421 plot.getRangeAxisEdge()); 422 423 // avoid possible sun.dc.pr.PRException: endPath: bad path 424 transY0 = restrictValueToDataArea(transY0, plot, dataArea); 425 426 if (Double.isNaN(y1)) { 427 // NULL value -> insert point on base line 428 // instead of 'step point' 429 transX1 = transX0; 430 transY0 = transY1; 431 } 432 if (transY0 != transY1) { 433 // not just a horizontal bar but need to perform a 'step'. 434 if (orientation == PlotOrientation.VERTICAL) { 435 this.pArea.addPoint((int) transX1, (int) transY0); 436 } 437 else if (orientation == PlotOrientation.HORIZONTAL) { 438 this.pArea.addPoint((int) transY0, (int) transX1); 439 } 440 } 441 } 442 443 Shape shape = null; 444 if (!Double.isNaN(y1)) { 445 // Add each point to Area (x, y) 446 if (orientation == PlotOrientation.VERTICAL) { 447 this.pArea.addPoint((int) transX1, (int) transY1); 448 } 449 else if (orientation == PlotOrientation.HORIZONTAL) { 450 this.pArea.addPoint((int) transY1, (int) transX1); 451 } 452 453 if (getShapesVisible()) { 454 shape = getItemShape(series, item); 455 if (orientation == PlotOrientation.VERTICAL) { 456 shape = ShapeUtilities.createTranslatedShape(shape, 457 transX1, transY1); 458 } 459 else if (orientation == PlotOrientation.HORIZONTAL) { 460 shape = ShapeUtilities.createTranslatedShape(shape, 461 transY1, transX1); 462 } 463 if (isShapesFilled()) { 464 g2.fill(shape); 465 } 466 else { 467 g2.draw(shape); 468 } 469 } 470 else { 471 if (orientation == PlotOrientation.VERTICAL) { 472 shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 473 4.0, 4.0); 474 } 475 else if (orientation == PlotOrientation.HORIZONTAL) { 476 shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 477 4.0, 4.0); 478 } 479 } 480 } 481 482 // Check if the item is the last item for the series or if it 483 // is a NULL value and number of items > 0. We can't draw an area for 484 // a single point. 485 if (getPlotArea() && item > 0 && this.pArea != null 486 && (item == (itemCount - 1) || Double.isNaN(y1))) { 487 488 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 489 plot.getRangeAxisEdge()); 490 491 // avoid possible sun.dc.pr.PRException: endPath: bad path 492 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 493 494 if (orientation == PlotOrientation.VERTICAL) { 495 // Add the last point (x,0) 496 this.pArea.addPoint((int) transX1, (int) transY2); 497 } 498 else if (orientation == PlotOrientation.HORIZONTAL) { 499 // Add the last point (x,0) 500 this.pArea.addPoint((int) transY2, (int) transX1); 501 } 502 503 // fill the polygon 504 g2.fill(this.pArea); 505 506 // draw an outline around the Area. 507 if (isOutline()) { 508 g2.setStroke(plot.getOutlineStroke()); 509 g2.setPaint(plot.getOutlinePaint()); 510 g2.draw(this.pArea); 511 } 512 513 // start new area when needed (see above) 514 this.pArea = null; 515 } 516 517 // do we need to update the crosshair values? 518 if (!Double.isNaN(y1)) { 519 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 520 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 521 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 522 rangeAxisIndex, transX1, transY1, orientation); 523 } 524 525 // collect entity and tool tip information... 526 if (state.getInfo() != null) { 527 EntityCollection entities = state.getEntityCollection(); 528 if (entities != null && shape != null) { 529 String tip = null; 530 XYToolTipGenerator generator 531 = getToolTipGenerator(series, item); 532 if (generator != null) { 533 tip = generator.generateToolTip(dataset, series, item); 534 } 535 String url = null; 536 if (getURLGenerator() != null) { 537 url = getURLGenerator().generateURL(dataset, series, item); 538 } 539 XYItemEntity entity = new XYItemEntity(shape, dataset, series, 540 item, tip, url); 541 entities.add(entity); 542 } 543 } 544 } 545 546 /** 547 * Tests this renderer for equality with an arbitrary object. 548 * 549 * @param obj the object (<code>null</code> permitted). 550 * 551 * @return A boolean. 552 */ 553 public boolean equals(Object obj) { 554 if (obj == this) { 555 return true; 556 } 557 if (!(obj instanceof XYStepAreaRenderer)) { 558 return false; 559 } 560 XYStepAreaRenderer that = (XYStepAreaRenderer) obj; 561 if (this.showOutline != that.showOutline) { 562 return false; 563 } 564 if (this.shapesVisible != that.shapesVisible) { 565 return false; 566 } 567 if (this.shapesFilled != that.shapesFilled) { 568 return false; 569 } 570 if (this.plotArea != that.plotArea) { 571 return false; 572 } 573 if (this.rangeBase != that.rangeBase) { 574 return false; 575 } 576 return super.equals(obj); 577 } 578 579 /** 580 * Returns a clone of the renderer. 581 * 582 * @return A clone. 583 * 584 * @throws CloneNotSupportedException if the renderer cannot be cloned. 585 */ 586 public Object clone() throws CloneNotSupportedException { 587 return super.clone(); 588 } 589 590 /** 591 * Helper method which returns a value if it lies 592 * inside the visible dataArea and otherwise the corresponding 593 * coordinate on the border of the dataArea. The PlotOrientation 594 * is taken into account. 595 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path 596 * which occurs when trying to draw lines/shapes which in large part 597 * lie outside of the visible dataArea. 598 * 599 * @param value the value which shall be 600 * @param dataArea the area within which the data is being drawn. 601 * @param plot the plot (can be used to obtain standard color 602 * information etc). 603 * @return <code>double</code> value inside the data area. 604 */ 605 protected static double restrictValueToDataArea(double value, 606 XYPlot plot, 607 Rectangle2D dataArea) { 608 double min = 0; 609 double max = 0; 610 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 611 min = dataArea.getMinY(); 612 max = dataArea.getMaxY(); 613 } 614 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 615 min = dataArea.getMinX(); 616 max = dataArea.getMaxX(); 617 } 618 if (value < min) { 619 value = min; 620 } 621 else if (value > max) { 622 value = max; 623 } 624 return value; 625 } 626 627 }