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 * XYBezierRenderer.java 029 * --------------------- 030 * (C) Copyright, by Javier Robes and Contributors. 031 * 032 * Original Author: Javier Robes; 033 * 034 */ 035 036package org.jfree.chart.renderer.xy; 037 038import java.awt.GradientPaint; 039import java.awt.Graphics2D; 040import java.awt.Paint; 041import java.awt.geom.GeneralPath; 042import java.awt.geom.Point2D; 043import java.awt.geom.Rectangle2D; 044import java.util.ArrayList; 045import java.util.List; 046import java.util.Objects; 047 048import org.jfree.chart.axis.ValueAxis; 049import org.jfree.chart.event.RendererChangeEvent; 050import org.jfree.chart.plot.PlotOrientation; 051import org.jfree.chart.plot.PlotRenderingInfo; 052import org.jfree.chart.plot.XYPlot; 053import org.jfree.chart.ui.GradientPaintTransformer; 054import org.jfree.chart.ui.RectangleEdge; 055import org.jfree.chart.ui.StandardGradientPaintTransformer; 056import org.jfree.chart.util.Args; 057import org.jfree.data.xy.XYDataset; 058 059 060/** 061 * A renderer that connects data points with Bezier cubic curves and/or 062 * draws shapes at each data point. This renderer is designed for use with 063 * the {@link XYPlot} class. 064 * <br><br> 065 * 066 * @since 067 */ 068public class XYBezierRenderer extends XYLineAndShapeRenderer { 069 070 /** 071 * An enumeration of the fill types for the renderer. 072 * 073 * @since 1.0.17 074 */ 075 public static enum FillType { 076 077 /** No fill. */ 078 NONE, 079 080 /** Fill down to zero. */ 081 TO_ZERO, 082 083 /** Fill to the lower bound. */ 084 TO_LOWER_BOUND, 085 086 /** Fill to the upper bound. */ 087 TO_UPPER_BOUND 088 } 089 090 /** 091 * Represents state information that applies to a single rendering of 092 * a chart. 093 */ 094 public static class XYBezierState extends State { 095 096 /** The area to fill under the curve. */ 097 public GeneralPath fillArea; 098 099 /** The points. */ 100 public List<Point2D> points; 101 102 /** 103 * Creates a new state instance. 104 * 105 * @param info the plot rendering info. 106 */ 107 public XYBezierState(PlotRenderingInfo info) { 108 super(info); 109 this.fillArea = new GeneralPath(); 110 this.points = new ArrayList<>(); 111 } 112 } 113 114 /** 115 * Resolution of Bezier curves (number of line segments between points) 116 */ 117 private int precision; 118 119 /** 120 * Tension defines how sharply does the curve bends 121 */ 122 private double tension; 123 124 /** 125 * A flag that can be set to specify 126 * to fill the area under the Bezier curve. 127 */ 128 private FillType fillType; 129 130 private GradientPaintTransformer gradientPaintTransformer; 131 132 /** 133 * Creates a new instance with the precision attribute defaulting to 5, 134 * the tension attribute defaulting to 2 135 * and no fill of the area 'under' the Bezier curve. 136 */ 137 public XYBezierRenderer() { 138 this(5, 25, FillType.NONE); 139 } 140 141 /** 142 * Creates a new renderer with the specified precision and tension 143 * and no fill of the area 'under' (between '0' and) the Bezier curve. 144 * 145 * @param precision the number of points between data items. 146 * @param tension value to define how sharply the curve bends 147 */ 148 public XYBezierRenderer(int precision, double tension) { 149 this(precision, tension ,FillType.NONE); 150 } 151 152 /** 153 * Creates a new renderer with the specified precision 154 * and specified fill of the area 'under' (between '0' and) the Bezier curve. 155 * 156 * @param precision the number of points between data items. 157 * @param tension value to define how sharply the Bezier curve bends 158 * @param fillType the type of fill beneath the curve ({@code null} 159 * not permitted). 160 * 161 * @since 1.0.17 162 */ 163 public XYBezierRenderer(int precision, double tension, FillType fillType) { 164 super(); 165 if (precision <= 0) { 166 throw new IllegalArgumentException("Requires precision > 0."); 167 } 168 if (tension <= 0) { 169 throw new IllegalArgumentException("Requires precision > 0."); 170 } 171 Args.nullNotPermitted(fillType, "fillType"); 172 this.precision = precision; 173 this.tension = tension; 174 this.fillType = fillType; 175 this.gradientPaintTransformer = new StandardGradientPaintTransformer(); 176 } 177 178 /** 179 * Returns the number of line segments used to approximate the Bezier 180 * curve between data points. 181 * 182 * @return The number of line segments. 183 * 184 * @see #setPrecision(int) 185 */ 186 public int getPrecision() { 187 return this.precision; 188 } 189 190 /** 191 * Set the resolution of Bezier curves and sends a {@link RendererChangeEvent} 192 * to all registered listeners. 193 * 194 * @param p number of line segments between points (must be > 0). 195 * 196 * @see #getPrecision() 197 */ 198 public void setPrecision(int p) { 199 if (p <= 0) { 200 throw new IllegalArgumentException("Requires p > 0."); 201 } 202 this.precision = p; 203 fireChangeEvent(); 204 } 205 206 /** 207 * Returns the value of the tension which defines how sharply 208 * does the curve bends 209 * 210 * @return The value of tesion. 211 * 212 * @see #setTension(double) 213 */ 214 public double getTension() { 215 return this.tension; 216 } 217 218 /** 219 * Set the value of the tension which defines how sharply 220 * does the curve bends and sends a {@link RendererChangeEvent} 221 * to all registered listeners. 222 * 223 * @param t value of tension (must be > 0). 224 * 225 * @see #getTension() 226 */ 227 public void setTension(double t) { 228 if (t <= 0) { 229 throw new IllegalArgumentException("Requires tension > 0."); 230 } 231 this.tension = t; 232 fireChangeEvent(); 233 } 234 235 /** 236 * Returns the type of fill that the renderer draws beneath the curve. 237 * 238 * @return The type of fill (never {@code null}). 239 * 240 * @see #setFillType(FillType) 241 * 242 * @since 1.0.17 243 */ 244 public FillType getFillType() { 245 return this.fillType; 246 } 247 248 /** 249 * Set the fill type and sends a {@link RendererChangeEvent} 250 * to all registered listeners. 251 * 252 * @param fillType the fill type ({@code null} not permitted). 253 * 254 * @see #getFillType() 255 * 256 * @since 1.0.17 257 */ 258 public void setFillType(FillType fillType) { 259 this.fillType = fillType; 260 fireChangeEvent(); 261 } 262 263 /** 264 * Returns the gradient paint transformer, or {@code null}. 265 * 266 * @return The gradient paint transformer (possibly {@code null}). 267 * 268 * @since 1.0.17 269 */ 270 public GradientPaintTransformer getGradientPaintTransformer() { 271 return this.gradientPaintTransformer; 272 } 273 274 /** 275 * Sets the gradient paint transformer and sends a 276 * {@link RendererChangeEvent} to all registered listeners. 277 * 278 * @param gpt the transformer ({@code null} permitted). 279 * 280 * @since 1.0.17 281 */ 282 public void setGradientPaintTransformer(GradientPaintTransformer gpt) { 283 this.gradientPaintTransformer = gpt; 284 fireChangeEvent(); 285 } 286 287 /** 288 * Initialises the renderer. 289 * <P> 290 * This method will be called before the first item is rendered, giving the 291 * renderer an opportunity to initialise any state information it wants to 292 * maintain. The renderer can do nothing if it chooses. 293 * 294 * @param g2 the graphics device. 295 * @param dataArea the area inside the axes. 296 * @param plot the plot. 297 * @param data the data. 298 * @param info an optional info collection object to return data back to 299 * the caller. 300 * 301 * @return The renderer state. 302 */ 303 @Override 304 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 305 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 306 307 setDrawSeriesLineAsPath(true); 308 XYBezierState state = new XYBezierState(info); 309 state.setProcessVisibleItemsOnly(false); 310 return state; 311 } 312 313 314 /** 315 * Draws the item (first pass). This method draws the lines 316 * connecting the items. Instead of drawing separate lines, 317 * a GeneralPath is constructed and drawn at the end of 318 * the series painting. 319 * 320 * @param g2 the graphics device. 321 * @param state the renderer state. 322 * @param plot the plot (can be used to obtain standard color information 323 * etc). 324 * @param dataset the dataset. 325 * @param pass the pass. 326 * @param series the series index (zero-based). 327 * @param item the item index (zero-based). 328 * @param xAxis the domain axis. 329 * @param yAxis the range axis. 330 * @param dataArea the area within which the data is being drawn. 331 */ 332 @Override 333 protected void drawPrimaryLineAsPath(XYItemRendererState state, 334 Graphics2D g2, XYPlot plot, XYDataset dataset, int pass, 335 int series, int item, ValueAxis xAxis, ValueAxis yAxis, 336 Rectangle2D dataArea) { 337 338 XYBezierState s = (XYBezierState) state; 339 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 340 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 341 342 // get the data points 343 double x1 = dataset.getXValue(series, item); 344 double y1 = dataset.getYValue(series, item); 345 double transX1 = xAxis.valueToJava2D(x1, dataArea, xAxisLocation); 346 double transY1 = yAxis.valueToJava2D(y1, dataArea, yAxisLocation); 347 348 // Collect points 349 if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) { 350 Point2D p = plot.getOrientation() == PlotOrientation.HORIZONTAL 351 ? new Point2D.Float((float) transY1, (float) transX1) 352 : new Point2D.Float((float) transX1, (float) transY1); 353 if (!s.points.contains(p)) 354 s.points.add(p); 355 } 356 357 if (item == dataset.getItemCount(series) - 1) { // construct path 358 if (s.points.size() > 1) { 359 Point2D origin; 360 if (this.fillType == FillType.TO_ZERO) { 361 float xz = (float) xAxis.valueToJava2D(0, dataArea, 362 yAxisLocation); 363 float yz = (float) yAxis.valueToJava2D(0, dataArea, 364 yAxisLocation); 365 origin = plot.getOrientation() == PlotOrientation.HORIZONTAL 366 ? new Point2D.Float(yz, xz) 367 : new Point2D.Float(xz, yz); 368 } else if (this.fillType == FillType.TO_LOWER_BOUND) { 369 float xlb = (float) xAxis.valueToJava2D( 370 xAxis.getLowerBound(), dataArea, xAxisLocation); 371 float ylb = (float) yAxis.valueToJava2D( 372 yAxis.getLowerBound(), dataArea, yAxisLocation); 373 origin = plot.getOrientation() == PlotOrientation.HORIZONTAL 374 ? new Point2D.Float(ylb, xlb) 375 : new Point2D.Float(xlb, ylb); 376 } else {// fillType == TO_UPPER_BOUND 377 float xub = (float) xAxis.valueToJava2D( 378 xAxis.getUpperBound(), dataArea, xAxisLocation); 379 float yub = (float) yAxis.valueToJava2D( 380 yAxis.getUpperBound(), dataArea, yAxisLocation); 381 origin = plot.getOrientation() == PlotOrientation.HORIZONTAL 382 ? new Point2D.Float(yub, xub) 383 : new Point2D.Float(xub, yub); 384 } 385 386 // we need at least two points to draw something 387 Point2D cp0 = s.points.get(0); 388 s.seriesPath.moveTo(cp0.getX(), cp0.getY()); 389 if (this.fillType != FillType.NONE) { 390 if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 391 s.fillArea.moveTo(origin.getX(), cp0.getY()); 392 } else { 393 s.fillArea.moveTo(cp0.getX(), origin.getY()); 394 } 395 s.fillArea.lineTo(cp0.getX(), cp0.getY()); 396 } 397 if (s.points.size() == 2) { 398 // we need at least 3 points to Bezier. Draw simple line 399 // for two points 400 Point2D cp1 = s.points.get(1); 401 if (this.fillType != FillType.NONE) { 402 s.fillArea.lineTo(cp1.getX(), cp1.getY()); 403 s.fillArea.lineTo(cp1.getX(), origin.getY()); 404 s.fillArea.closePath(); 405 } 406 s.seriesPath.lineTo(cp1.getX(), cp1.getY()); 407 } 408 else if (s.points.size() == 3) { 409 // with 3 points only initial and end Bezier curves are required. 410 411 Point2D[] pInitial = getInitalPoints(s); 412 addBezierPointsToSeriesPath(pInitial, s); 413 Point2D[] pFinal = getFinalPoints(s); 414 addBezierPointsToSeriesPath(pFinal, s); 415 416 } 417 else { 418 // construct Bezier curve 419 int np = s.points.size(); // number of points 420 for(int i = 0; i < np - 1; i++) { 421 if(i == 0) { 422 // 3 points, 2 lines (initial an final Bezier curves) 423 Point2D[] initial3Points = new Point2D[3]; 424 initial3Points[0] = s.points.get(0); 425 initial3Points[1] = s.points.get(1); 426 initial3Points[2] = s.points.get(2); 427 Point2D[] pInitial = calcSegmentPointsInitial(initial3Points); 428 addBezierPointsToSeriesPath(pInitial, s); 429 } 430 if(i == np - 2) { 431 Point2D[] final3Points = new Point2D[4]; 432 final3Points[1] = s.points.get(np-3); 433 final3Points[2] = s.points.get(np-2); 434 final3Points[3] = s.points.get(np-1); 435 // No need for final3Points[0]. Not required 436 Point2D[] pFinal = calcSegmentPointsFinal(final3Points); 437 addBezierPointsToSeriesPath(pFinal, s); 438 } 439 if ((i != 0) && (i != (np - 2))){ 440 Point2D[] original4Points = new Point2D[4]; 441 original4Points[0] = s.points.get(i - 1); 442 original4Points[1] = s.points.get(i); 443 original4Points[2] = s.points.get(i + 1); 444 original4Points[3] = s.points.get(i + 2); 445 Point2D[] pMedium = calculateSegmentPoints(original4Points); 446 addBezierPointsToSeriesPath(pMedium, s); 447 } 448 } 449 } 450 // Add last point @ y=0 for fillPath and close path 451 if (this.fillType != FillType.NONE) { 452 if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 453 s.fillArea.lineTo(origin.getX(), s.points.get( 454 s.points.size() - 1).getY()); 455 } else { 456 s.fillArea.lineTo(s.points.get( 457 s.points.size() - 1).getX(), origin.getY()); 458 } 459 s.fillArea.closePath(); 460 } 461 // fill under the curve... 462 if (this.fillType != FillType.NONE) { 463 Paint fp = getSeriesFillPaint(series); 464 if (this.gradientPaintTransformer != null 465 && fp instanceof GradientPaint) { 466 GradientPaint gp = this.gradientPaintTransformer 467 .transform((GradientPaint) fp, s.fillArea); 468 g2.setPaint(gp); 469 } else { 470 g2.setPaint(fp); 471 } 472 g2.fill(s.fillArea); 473 s.fillArea.reset(); 474 } 475 // then draw the line... 476 drawFirstPassShape(g2, pass, series, item, s.seriesPath); 477 } 478 // reset points vector 479 s.points = new ArrayList<>(); 480 } 481 } 482 483 private void addBezierPointsToSeriesPath(Point2D[] segmentPoints, XYBezierState s) { 484 double x; 485 double y; 486 for (int t = 0 ; t <= this.precision; t++) { 487 double k = (double)t / this.precision; 488 double r = 1- k; 489 490 x = Math.pow(r, 3) * segmentPoints[0].getX() + 3 * k * Math.pow(r, 2) * segmentPoints[1].getX() 491 + 3 * Math.pow(k, 2) * (1 - k) * segmentPoints[2].getX() + Math.pow(k, 3) * segmentPoints[3].getX(); 492 y = Math.pow(r, 3) * segmentPoints[0].getY() + 3 * k * Math.pow(r, 2) * segmentPoints[1].getY() 493 + 3 * Math.pow(k, 2) * (1 - k) * segmentPoints[2].getY() + Math.pow(k, 3) * segmentPoints[3].getY(); 494 s.seriesPath.lineTo(x, y); 495 if (this.fillType != FillType.NONE) { 496 s.fillArea.lineTo(x, y); 497 } 498 } 499 } 500 501 private Point2D[] getFinalPoints(XYBezierState s) { 502 Point2D[] final3Points = new Point2D[4]; 503 final3Points[1] = s.points.get(0); 504 final3Points[2] = s.points.get(1); 505 final3Points[3] = s.points.get(2); 506 // No need for final3Points[0]. Not required 507 Point2D[] pFinal = calcSegmentPointsFinal(final3Points);//TENSION = 1.5 508 return pFinal; 509 } 510 511 private Point2D[] getInitalPoints(XYBezierState s) { 512 Point2D[] initial3Points = new Point2D[3]; 513 initial3Points[0] = s.points.get(0); 514 initial3Points[1] = s.points.get(1); 515 initial3Points[2] = s.points.get(2); 516 Point2D[] pInitial = calcSegmentPointsInitial(initial3Points);// TENSION = 1.5 517 return pInitial; 518 } 519 520 private Point2D[] calculateSegmentPoints(Point2D[] original4Points) { 521 Point2D[] points = new Point2D[4]; 522 points[0] = original4Points[1]; 523 points[3] = original4Points[2]; 524 for(int i = 1; i < 3; i++) { 525 Point2D aux1 = calcUnitaryVector(original4Points[i-1], original4Points[i]); 526 Point2D aux2 = calcUnitaryVector(original4Points[i+1], original4Points[i]); 527 Point2D aux3 = calcUnitaryVector(aux2, aux1); 528 529 double x = original4Points[i].getX() + Math.pow(-1.0, i+1) * tension * aux3.getX(); 530 double y = original4Points[i].getY() + Math.pow(-1.0, i+1) * tension * aux3.getY(); 531 points[i] = new Point2D.Double(x, y); 532 } 533 return points; 534 } 535 536 private Point2D[] calcSegmentPointsInitial(Point2D[] original3P) { 537 Point2D[] points = new Point2D[4]; 538 points[0] = original3P[0];// Endpoint 1 539 points[3] = original3P[1];// Endpoint 2 540 // Control point 1 541 Point2D auxInitial = calcUnitaryVector(original3P[0], original3P[1]); 542 points[1] = original3P[0];// new Point2D.Double(x0, y0); 543 // Control point 2 544 Point2D aux2 = calcUnitaryVector(original3P[2], original3P[1]); 545 Point2D aux3 = calcUnitaryVector(auxInitial, aux2); 546 double x = original3P[1].getX() + tension * aux3.getX(); 547 double y = original3P[1].getY() + tension * aux3.getY(); 548 points[2] = new Point2D.Double(x, y); 549 return points; 550 } 551 552 private Point2D[] calcSegmentPointsFinal(Point2D[] original3P) { 553 /* 554 * Each segment is defined by its two endpoints and two control points. A 555 * control point determines the tangent at the corresponding endpoint. 556 */ 557 Point2D[] points = new Point2D[4]; 558 points[0] = original3P[2];// Endpoint 1 559 points[3] = original3P[3];// Endpoint 2 560 // Control point 2: points[2] 561 Point2D auxInitial = calcUnitaryVector(original3P[3], original3P[2]); 562 points[2] = original3P[3];// new Point2D.Double(x0, y0); 563 // Control point 1 564 Point2D aux1 = calcUnitaryVector(original3P[3], original3P[2]); 565 Point2D aux2 = calcUnitaryVector(original3P[1], original3P[2]); 566 Point2D aux3 = calcUnitaryVector(aux1, aux2); 567 double x = original3P[2].getX() + tension * aux3.getX(); 568 double y = original3P[2].getY() + tension * aux3.getY(); 569 points[1] = new Point2D.Double(x, y); 570 return points; 571 } 572 573 private Point2D calcUnitaryVector(Point2D pOrigin, Point2D pEnd) { 574 double module = Math.sqrt(Math.pow(pEnd.getX() - pOrigin.getX(), 2) + 575 Math.pow(pEnd.getY() - pOrigin.getY(), 2)); 576 if (module == 0) { 577 return null; 578 } 579 return new Point2D.Double((pEnd.getX() - pOrigin.getX()) / module, 580 (pEnd.getY() - pOrigin.getY()) /module); 581 } 582 583 584 /** 585 * Tests this renderer for equality with an arbitrary object. 586 * 587 * @param obj the object ({@code null} permitted). 588 * 589 * @return A boolean. 590 */ 591 @Override 592 public boolean equals(Object obj) { 593 if (obj == this) { 594 return true; 595 } 596 if (!(obj instanceof XYBezierRenderer)) { 597 return false; 598 } 599 XYBezierRenderer that = (XYBezierRenderer) obj; 600 if (this.precision != that.precision) { 601 return false; 602 } 603 if (this.fillType != that.fillType) { 604 return false; 605 } 606 if (!Objects.equals(this.gradientPaintTransformer, that.gradientPaintTransformer)) { 607 return false; 608 } 609 return super.equals(obj); 610 } 611}