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