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.5 2007/02/06 16:29:11 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 * 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 public boolean isOutline() { 177 return this.showOutline; 178 } 179 180 /** 181 * Sets a flag that controls whether or not outlines of the areas are 182 * drawn, and sends a {@link RendererChangeEvent} to all registered 183 * listeners. 184 * 185 * @param show the flag. 186 */ 187 public void setOutline(boolean show) { 188 this.showOutline = show; 189 notifyListeners(new RendererChangeEvent(this)); 190 } 191 192 /** 193 * Returns true if shapes are being plotted by the renderer. 194 * 195 * @return <code>true</code> if shapes are being plotted by the renderer. 196 */ 197 public boolean getShapesVisible() { 198 return this.shapesVisible; 199 } 200 201 /** 202 * Sets the flag that controls whether or not shapes are displayed for each 203 * data item, and sends a {@link RendererChangeEvent} to all registered 204 * listeners. 205 * 206 * @param flag the flag. 207 */ 208 public void setShapesVisible(boolean flag) { 209 this.shapesVisible = flag; 210 notifyListeners(new RendererChangeEvent(this)); 211 } 212 213 /** 214 * Returns the flag that controls whether or not the shapes are filled. 215 * 216 * @return A boolean. 217 */ 218 public boolean isShapesFilled() { 219 return this.shapesFilled; 220 } 221 222 /** 223 * Sets the 'shapes filled' for ALL series. 224 * 225 * @param filled the flag. 226 */ 227 public void setShapesFilled(boolean filled) { 228 this.shapesFilled = filled; 229 notifyListeners(new RendererChangeEvent(this)); 230 } 231 232 /** 233 * Returns true if Area is being plotted by the renderer. 234 * 235 * @return <code>true</code> if Area is being plotted by the renderer. 236 */ 237 public boolean getPlotArea() { 238 return this.plotArea; 239 } 240 241 /** 242 * Sets a flag that controls whether or not areas are drawn for each data 243 * item. 244 * 245 * @param flag the flag. 246 */ 247 public void setPlotArea(boolean flag) { 248 this.plotArea = flag; 249 notifyListeners(new RendererChangeEvent(this)); 250 } 251 252 /** 253 * Returns the value on the range axis which defines the 'lower' border of 254 * the area. 255 * 256 * @return <code>double</code> the value on the range axis which defines 257 * the 'lower' border of the area. 258 */ 259 public double getRangeBase() { 260 return this.rangeBase; 261 } 262 263 /** 264 * Sets the value on the range axis which defines the default border of the 265 * area. E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 266 * reach the lower border of the plotArea. 267 * 268 * @param val the value on the range axis which defines the default border 269 * of the area. 270 */ 271 public void setRangeBase(double val) { 272 this.rangeBase = val; 273 notifyListeners(new RendererChangeEvent(this)); 274 } 275 276 /** 277 * Initialises the renderer. Here we calculate the Java2D y-coordinate for 278 * zero, since all the bars have their bases fixed at zero. 279 * 280 * @param g2 the graphics device. 281 * @param dataArea the area inside the axes. 282 * @param plot the plot. 283 * @param data the data. 284 * @param info an optional info collection object to return data back to 285 * the caller. 286 * 287 * @return The number of passes required by the renderer. 288 */ 289 public XYItemRendererState initialise(Graphics2D g2, 290 Rectangle2D dataArea, 291 XYPlot plot, 292 XYDataset data, 293 PlotRenderingInfo info) { 294 295 return super.initialise(g2, dataArea, plot, data, info); 296 297 } 298 299 300 /** 301 * Draws the visual representation of a single data item. 302 * 303 * @param g2 the graphics device. 304 * @param state the renderer state. 305 * @param dataArea the area within which the data is being drawn. 306 * @param info collects information about the drawing. 307 * @param plot the plot (can be used to obtain standard color information 308 * etc). 309 * @param domainAxis the domain axis. 310 * @param rangeAxis the range axis. 311 * @param dataset the dataset. 312 * @param series the series index (zero-based). 313 * @param item the item index (zero-based). 314 * @param crosshairState crosshair information for the plot 315 * (<code>null</code> permitted). 316 * @param pass the pass index. 317 */ 318 public void drawItem(Graphics2D g2, 319 XYItemRendererState state, 320 Rectangle2D dataArea, 321 PlotRenderingInfo info, 322 XYPlot plot, 323 ValueAxis domainAxis, 324 ValueAxis rangeAxis, 325 XYDataset dataset, 326 int series, 327 int item, 328 CrosshairState crosshairState, 329 int pass) { 330 331 PlotOrientation orientation = plot.getOrientation(); 332 333 // Get the item count for the series, so that we can know which is the 334 // end of the series. 335 int itemCount = dataset.getItemCount(series); 336 337 Paint paint = getItemPaint(series, item); 338 Stroke seriesStroke = getItemStroke(series, item); 339 g2.setPaint(paint); 340 g2.setStroke(seriesStroke); 341 342 // get the data point... 343 double x1 = dataset.getXValue(series, item); 344 double y1 = dataset.getYValue(series, item); 345 double x = x1; 346 double y = Double.isNaN(y1) ? getRangeBase() : y1; 347 double transX1 = domainAxis.valueToJava2D(x, dataArea, 348 plot.getDomainAxisEdge()); 349 double transY1 = rangeAxis.valueToJava2D(y, dataArea, 350 plot.getRangeAxisEdge()); 351 352 // avoid possible sun.dc.pr.PRException: endPath: bad path 353 transY1 = restrictValueToDataArea(transY1, plot, dataArea); 354 355 if (this.pArea == null && !Double.isNaN(y1)) { 356 357 // Create a new Area for the series 358 this.pArea = new Polygon(); 359 360 // start from Y = rangeBase 361 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 362 plot.getRangeAxisEdge()); 363 364 // avoid possible sun.dc.pr.PRException: endPath: bad path 365 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 366 367 // The first point is (x, this.baseYValue) 368 if (orientation == PlotOrientation.VERTICAL) { 369 this.pArea.addPoint((int) transX1, (int) transY2); 370 } 371 else if (orientation == PlotOrientation.HORIZONTAL) { 372 this.pArea.addPoint((int) transY2, (int) transX1); 373 } 374 } 375 376 double transX0 = 0; 377 double transY0 = restrictValueToDataArea( 378 getRangeBase(), plot, dataArea 379 ); 380 381 double x0; 382 double y0; 383 if (item > 0) { 384 // get the previous data point... 385 x0 = dataset.getXValue(series, item - 1); 386 y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1); 387 388 x = x0; 389 y = Double.isNaN(y0) ? getRangeBase() : y0; 390 transX0 = domainAxis.valueToJava2D(x, dataArea, 391 plot.getDomainAxisEdge()); 392 transY0 = rangeAxis.valueToJava2D(y, dataArea, 393 plot.getRangeAxisEdge()); 394 395 // avoid possible sun.dc.pr.PRException: endPath: bad path 396 transY0 = restrictValueToDataArea(transY0, plot, dataArea); 397 398 if (Double.isNaN(y1)) { 399 // NULL value -> insert point on base line 400 // instead of 'step point' 401 transX1 = transX0; 402 transY0 = transY1; 403 } 404 if (transY0 != transY1) { 405 // not just a horizontal bar but need to perform a 'step'. 406 if (orientation == PlotOrientation.VERTICAL) { 407 this.pArea.addPoint((int) transX1, (int) transY0); 408 } 409 else if (orientation == PlotOrientation.HORIZONTAL) { 410 this.pArea.addPoint((int) transY0, (int) transX1); 411 } 412 } 413 } 414 415 Shape shape = null; 416 if (!Double.isNaN(y1)) { 417 // Add each point to Area (x, y) 418 if (orientation == PlotOrientation.VERTICAL) { 419 this.pArea.addPoint((int) transX1, (int) transY1); 420 } 421 else if (orientation == PlotOrientation.HORIZONTAL) { 422 this.pArea.addPoint((int) transY1, (int) transX1); 423 } 424 425 if (getShapesVisible()) { 426 shape = getItemShape(series, item); 427 if (orientation == PlotOrientation.VERTICAL) { 428 shape = ShapeUtilities.createTranslatedShape(shape, 429 transX1, transY1); 430 } 431 else if (orientation == PlotOrientation.HORIZONTAL) { 432 shape = ShapeUtilities.createTranslatedShape(shape, 433 transY1, transX1); 434 } 435 if (isShapesFilled()) { 436 g2.fill(shape); 437 } 438 else { 439 g2.draw(shape); 440 } 441 } 442 else { 443 if (orientation == PlotOrientation.VERTICAL) { 444 shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 445 4.0, 4.0); 446 } 447 else if (orientation == PlotOrientation.HORIZONTAL) { 448 shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 449 4.0, 4.0); 450 } 451 } 452 } 453 454 // Check if the item is the last item for the series or if it 455 // is a NULL value and number of items > 0. We can't draw an area for 456 // a single point. 457 if (getPlotArea() && item > 0 && this.pArea != null 458 && (item == (itemCount - 1) || Double.isNaN(y1))) { 459 460 double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 461 plot.getRangeAxisEdge()); 462 463 // avoid possible sun.dc.pr.PRException: endPath: bad path 464 transY2 = restrictValueToDataArea(transY2, plot, dataArea); 465 466 if (orientation == PlotOrientation.VERTICAL) { 467 // Add the last point (x,0) 468 this.pArea.addPoint((int) transX1, (int) transY2); 469 } 470 else if (orientation == PlotOrientation.HORIZONTAL) { 471 // Add the last point (x,0) 472 this.pArea.addPoint((int) transY2, (int) transX1); 473 } 474 475 // fill the polygon 476 g2.fill(this.pArea); 477 478 // draw an outline around the Area. 479 if (isOutline()) { 480 g2.setStroke(plot.getOutlineStroke()); 481 g2.setPaint(plot.getOutlinePaint()); 482 g2.draw(this.pArea); 483 } 484 485 // start new area when needed (see above) 486 this.pArea = null; 487 } 488 489 // do we need to update the crosshair values? 490 if (!Double.isNaN(y1)) { 491 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 492 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 493 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 494 rangeAxisIndex, transX1, transY1, orientation); 495 } 496 497 // collect entity and tool tip information... 498 if (state.getInfo() != null) { 499 EntityCollection entities = state.getEntityCollection(); 500 if (entities != null && shape != null) { 501 String tip = null; 502 XYToolTipGenerator generator 503 = getToolTipGenerator(series, item); 504 if (generator != null) { 505 tip = generator.generateToolTip(dataset, series, item); 506 } 507 String url = null; 508 if (getURLGenerator() != null) { 509 url = getURLGenerator().generateURL(dataset, series, item); 510 } 511 XYItemEntity entity = new XYItemEntity(shape, dataset, series, 512 item, tip, url); 513 entities.add(entity); 514 } 515 } 516 } 517 518 /** 519 * Returns a clone of the renderer. 520 * 521 * @return A clone. 522 * 523 * @throws CloneNotSupportedException if the renderer cannot be cloned. 524 */ 525 public Object clone() throws CloneNotSupportedException { 526 return super.clone(); 527 } 528 529 /** 530 * Helper method which returns a value if it lies 531 * inside the visible dataArea and otherwise the corresponding 532 * coordinate on the border of the dataArea. The PlotOrientation 533 * is taken into account. 534 * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path 535 * which occurs when trying to draw lines/shapes which in large part 536 * lie outside of the visible dataArea. 537 * 538 * @param value the value which shall be 539 * @param dataArea the area within which the data is being drawn. 540 * @param plot the plot (can be used to obtain standard color 541 * information etc). 542 * @return <code>double</code> value inside the data area. 543 */ 544 protected static double restrictValueToDataArea(double value, 545 XYPlot plot, 546 Rectangle2D dataArea) { 547 double min = 0; 548 double max = 0; 549 if (plot.getOrientation() == PlotOrientation.VERTICAL) { 550 min = dataArea.getMinY(); 551 max = dataArea.getMaxY(); 552 } 553 else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 554 min = dataArea.getMinX(); 555 max = dataArea.getMaxX(); 556 } 557 if (value < min) { 558 value = min; 559 } 560 else if (value > max) { 561 value = max; 562 } 563 return value; 564 } 565 566 }