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