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