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