001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, 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 * XYSplineRenderer.java 029 * --------------------- 030 * (C) Copyright 2007-2021, by Klaus Rheinwald and Contributors. 031 * 032 * Original Author: Klaus Rheinwald; 033 * Contributor(s): Tobias von Petersdorff (tvp@math.umd.edu, 034 * http://www.wam.umd.edu/~petersd/); 035 * David Gilbert; 036 * 037 */ 038 039package org.jfree.chart.renderer.xy; 040 041import java.awt.GradientPaint; 042import java.awt.Graphics2D; 043import java.awt.Paint; 044import java.awt.geom.GeneralPath; 045import java.awt.geom.Point2D; 046import java.awt.geom.Rectangle2D; 047import java.util.ArrayList; 048import java.util.List; 049import java.util.Objects; 050 051import org.jfree.chart.axis.ValueAxis; 052import org.jfree.chart.event.RendererChangeEvent; 053import org.jfree.chart.plot.PlotOrientation; 054import org.jfree.chart.plot.PlotRenderingInfo; 055import org.jfree.chart.plot.XYPlot; 056import org.jfree.chart.util.GradientPaintTransformer; 057import org.jfree.chart.api.RectangleEdge; 058import org.jfree.chart.util.StandardGradientPaintTransformer; 059import org.jfree.chart.internal.Args; 060import org.jfree.data.xy.XYDataset; 061 062/** 063 * A renderer that connects data points with natural cubic splines and/or 064 * draws shapes at each data point. This renderer is designed for use with 065 * the {@link XYPlot} class. The example shown here is generated by the 066 * {@code XYSplineRendererDemo1.java} program included in the JFreeChart 067 * demo collection: 068 * <br><br> 069 * <img src="doc-files/XYSplineRendererSample.png" alt="XYSplineRendererSample.png"> 070 * 071 * @since 1.0.7 072 */ 073public class XYSplineRenderer extends XYLineAndShapeRenderer { 074 075 /** 076 * An enumeration of the fill types for the renderer. 077 */ 078 public enum FillType { 079 080 /** No fill. */ 081 NONE, 082 083 /** Fill down to zero. */ 084 TO_ZERO, 085 086 /** Fill to the lower bound. */ 087 TO_LOWER_BOUND, 088 089 /** Fill to the upper bound. */ 090 TO_UPPER_BOUND 091 } 092 093 /** 094 * Represents state information that applies to a single rendering of 095 * a chart. 096 */ 097 public static class XYSplineState extends State { 098 099 /** The area to fill under the curve. */ 100 public GeneralPath fillArea; 101 102 /** The points. */ 103 public List<Point2D> points; 104 105 /** 106 * Creates a new state instance. 107 * 108 * @param info the plot rendering info. 109 */ 110 public XYSplineState(PlotRenderingInfo info) { 111 super(info); 112 this.fillArea = new GeneralPath(); 113 this.points = new ArrayList<>(); 114 } 115 } 116 117 /** 118 * Resolution of splines (number of line segments between points) 119 */ 120 private int precision; 121 122 /** 123 * A flag that can be set to specify 124 * to fill the area under the spline. 125 */ 126 private FillType fillType; 127 128 private GradientPaintTransformer gradientPaintTransformer; 129 130 /** 131 * Creates a new instance with the precision attribute defaulting to 5 132 * and no fill of the area 'under' the spline. 133 */ 134 public XYSplineRenderer() { 135 this(5, FillType.NONE); 136 } 137 138 /** 139 * Creates a new renderer with the specified precision 140 * and no fill of the area 'under' (between '0' and) the spline. 141 * 142 * @param precision the number of points between data items. 143 */ 144 public XYSplineRenderer(int precision) { 145 this(precision, FillType.NONE); 146 } 147 148 /** 149 * Creates a new renderer with the specified precision 150 * and specified fill of the area 'under' (between '0' and) the spline. 151 * 152 * @param precision the number of points between data items. 153 * @param fillType the type of fill beneath the curve ({@code null} 154 * not permitted). 155 * 156 * @since 1.0.17 157 */ 158 public XYSplineRenderer(int precision, FillType fillType) { 159 super(); 160 if (precision <= 0) { 161 throw new IllegalArgumentException("Requires precision > 0."); 162 } 163 Args.nullNotPermitted(fillType, "fillType"); 164 this.precision = precision; 165 this.fillType = fillType; 166 this.gradientPaintTransformer = new StandardGradientPaintTransformer(); 167 } 168 169 /** 170 * Returns the number of line segments used to approximate the spline 171 * curve between data points. 172 * 173 * @return The number of line segments. 174 * 175 * @see #setPrecision(int) 176 */ 177 public int getPrecision() { 178 return this.precision; 179 } 180 181 /** 182 * Set the resolution of splines and sends a {@link RendererChangeEvent} 183 * to all registered listeners. 184 * 185 * @param p number of line segments between points (must be > 0). 186 * 187 * @see #getPrecision() 188 */ 189 public void setPrecision(int p) { 190 if (p <= 0) { 191 throw new IllegalArgumentException("Requires p > 0."); 192 } 193 this.precision = p; 194 fireChangeEvent(); 195 } 196 197 /** 198 * Returns the type of fill that the renderer draws beneath the curve. 199 * 200 * @return The type of fill (never {@code null}). 201 * 202 * @see #setFillType(FillType) 203 * 204 * @since 1.0.17 205 */ 206 public FillType getFillType() { 207 return this.fillType; 208 } 209 210 /** 211 * Set the fill type and sends a {@link RendererChangeEvent} 212 * to all registered listeners. 213 * 214 * @param fillType the fill type ({@code null} not permitted). 215 * 216 * @see #getFillType() 217 * 218 * @since 1.0.17 219 */ 220 public void setFillType(FillType fillType) { 221 this.fillType = fillType; 222 fireChangeEvent(); 223 } 224 225 /** 226 * Returns the gradient paint transformer, or {@code null}. 227 * 228 * @return The gradient paint transformer (possibly {@code null}). 229 * 230 * @since 1.0.17 231 */ 232 public GradientPaintTransformer getGradientPaintTransformer() { 233 return this.gradientPaintTransformer; 234 } 235 236 /** 237 * Sets the gradient paint transformer and sends a 238 * {@link RendererChangeEvent} to all registered listeners. 239 * 240 * @param gpt the transformer ({@code null} permitted). 241 * 242 * @since 1.0.17 243 */ 244 public void setGradientPaintTransformer(GradientPaintTransformer gpt) { 245 this.gradientPaintTransformer = gpt; 246 fireChangeEvent(); 247 } 248 249 /** 250 * Initialises the renderer. 251 * <P> 252 * This method will be called before the first item is rendered, giving the 253 * renderer an opportunity to initialise any state information it wants to 254 * maintain. The renderer can do nothing if it chooses. 255 * 256 * @param g2 the graphics device. 257 * @param dataArea the area inside the axes. 258 * @param plot the plot. 259 * @param data the data. 260 * @param info an optional info collection object to return data back to 261 * the caller. 262 * 263 * @return The renderer state. 264 */ 265 @Override 266 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 267 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 268 269 setDrawSeriesLineAsPath(true); 270 XYSplineState state = new XYSplineState(info); 271 state.setProcessVisibleItemsOnly(false); 272 return state; 273 } 274 275 /** 276 * Draws the item (first pass). This method draws the lines 277 * connecting the items. Instead of drawing separate lines, 278 * a GeneralPath is constructed and drawn at the end of 279 * the series painting. 280 * 281 * @param g2 the graphics device. 282 * @param state the renderer state. 283 * @param plot the plot (can be used to obtain standard color information 284 * etc). 285 * @param dataset the dataset. 286 * @param pass the pass. 287 * @param series the series index (zero-based). 288 * @param item the item index (zero-based). 289 * @param xAxis the domain axis. 290 * @param yAxis the range axis. 291 * @param dataArea the area within which the data is being drawn. 292 */ 293 @Override 294 protected void drawPrimaryLineAsPath(XYItemRendererState state, 295 Graphics2D g2, XYPlot plot, XYDataset dataset, int pass, 296 int series, int item, ValueAxis xAxis, ValueAxis yAxis, 297 Rectangle2D dataArea) { 298 299 XYSplineState s = (XYSplineState) state; 300 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 301 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 302 303 // get the data points 304 double x1 = dataset.getXValue(series, item); 305 double y1 = dataset.getYValue(series, item); 306 double transX1 = xAxis.valueToJava2D(x1, dataArea, xAxisLocation); 307 double transY1 = yAxis.valueToJava2D(y1, dataArea, yAxisLocation); 308 309 // Collect points 310 if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) { 311 Point2D p = plot.getOrientation() == PlotOrientation.HORIZONTAL 312 ? new Point2D.Float((float) transY1, (float) transX1) 313 : new Point2D.Float((float) transX1, (float) transY1); 314 if (!s.points.contains(p)) 315 s.points.add(p); 316 } 317 318 if (item == dataset.getItemCount(series) - 1) { // construct path 319 if (s.points.size() > 1) { 320 Point2D origin; 321 if (this.fillType == FillType.TO_ZERO) { 322 float xz = (float) xAxis.valueToJava2D(0, dataArea, 323 yAxisLocation); 324 float yz = (float) yAxis.valueToJava2D(0, dataArea, 325 yAxisLocation); 326 origin = plot.getOrientation() == PlotOrientation.HORIZONTAL 327 ? new Point2D.Float(yz, xz) 328 : new Point2D.Float(xz, yz); 329 } else if (this.fillType == FillType.TO_LOWER_BOUND) { 330 float xlb = (float) xAxis.valueToJava2D( 331 xAxis.getLowerBound(), dataArea, xAxisLocation); 332 float ylb = (float) yAxis.valueToJava2D( 333 yAxis.getLowerBound(), dataArea, yAxisLocation); 334 origin = plot.getOrientation() == PlotOrientation.HORIZONTAL 335 ? new Point2D.Float(ylb, xlb) 336 : new Point2D.Float(xlb, ylb); 337 } else {// fillType == TO_UPPER_BOUND 338 float xub = (float) xAxis.valueToJava2D( 339 xAxis.getUpperBound(), dataArea, xAxisLocation); 340 float yub = (float) yAxis.valueToJava2D( 341 yAxis.getUpperBound(), dataArea, yAxisLocation); 342 origin = plot.getOrientation() == PlotOrientation.HORIZONTAL 343 ? new Point2D.Float(yub, xub) 344 : new Point2D.Float(xub, yub); 345 } 346 347 // we need at least two points to draw something 348 Point2D cp0 = s.points.get(0); 349 s.seriesPath.moveTo(cp0.getX(), cp0.getY()); 350 if (this.fillType != FillType.NONE) { 351 if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 352 s.fillArea.moveTo(origin.getX(), cp0.getY()); 353 } else { 354 s.fillArea.moveTo(cp0.getX(), origin.getY()); 355 } 356 s.fillArea.lineTo(cp0.getX(), cp0.getY()); 357 } 358 if (s.points.size() == 2) { 359 // we need at least 3 points to spline. Draw simple line 360 // for two points 361 Point2D cp1 = s.points.get(1); 362 if (this.fillType != FillType.NONE) { 363 s.fillArea.lineTo(cp1.getX(), cp1.getY()); 364 s.fillArea.lineTo(cp1.getX(), origin.getY()); 365 s.fillArea.closePath(); 366 } 367 s.seriesPath.lineTo(cp1.getX(), cp1.getY()); 368 } else { 369 // construct spline 370 int np = s.points.size(); // number of points 371 float[] d = new float[np]; // Newton form coefficients 372 float[] x = new float[np]; // x-coordinates of nodes 373 float y, oldy; 374 float t, oldt; 375 376 float[] a = new float[np]; 377 float t1; 378 float t2; 379 float[] h = new float[np]; 380 381 for (int i = 0; i < np; i++) { 382 Point2D.Float cpi = (Point2D.Float) s.points.get(i); 383 x[i] = cpi.x; 384 d[i] = cpi.y; 385 } 386 387 for (int i = 1; i <= np - 1; i++) 388 h[i] = x[i] - x[i - 1]; 389 390 float[] sub = new float[np - 1]; 391 float[] diag = new float[np - 1]; 392 float[] sup = new float[np - 1]; 393 394 for (int i = 1; i <= np - 2; i++) { 395 diag[i] = (h[i] + h[i + 1]) / 3; 396 sup[i] = h[i + 1] / 6; 397 sub[i] = h[i] / 6; 398 a[i] = (d[i + 1] - d[i]) / h[i + 1] 399 - (d[i] - d[i - 1]) / h[i]; 400 } 401 solveTridiag(sub, diag, sup, a, np - 2); 402 403 // note that a[0]=a[np-1]=0 404 oldt = x[0]; 405 oldy = d[0]; 406 for (int i = 1; i <= np - 1; i++) { 407 // loop over intervals between nodes 408 for (int j = 1; j <= this.precision; j++) { 409 t1 = (h[i] * j) / this.precision; 410 t2 = h[i] - t1; 411 y = ((-a[i - 1] / 6 * (t2 + h[i]) * t1 + d[i - 1]) 412 * t2 + (-a[i] / 6 * (t1 + h[i]) * t2 413 + d[i]) * t1) / h[i]; 414 t = x[i - 1] + t1; 415 s.seriesPath.lineTo(t, y); 416 if (this.fillType != FillType.NONE) { 417 s.fillArea.lineTo(t, y); 418 } 419 } 420 } 421 } 422 // Add last point @ y=0 for fillPath and close path 423 if (this.fillType != FillType.NONE) { 424 if (plot.getOrientation() == PlotOrientation.HORIZONTAL) { 425 s.fillArea.lineTo(origin.getX(), s.points.get( 426 s.points.size() - 1).getY()); 427 } else { 428 s.fillArea.lineTo(s.points.get( 429 s.points.size() - 1).getX(), origin.getY()); 430 } 431 s.fillArea.closePath(); 432 } 433 434 // fill under the curve... 435 if (this.fillType != FillType.NONE) { 436 Paint fp = getSeriesFillPaint(series); 437 if (this.gradientPaintTransformer != null 438 && fp instanceof GradientPaint) { 439 GradientPaint gp = this.gradientPaintTransformer 440 .transform((GradientPaint) fp, s.fillArea); 441 g2.setPaint(gp); 442 } else { 443 g2.setPaint(fp); 444 } 445 g2.fill(s.fillArea); 446 s.fillArea.reset(); 447 } 448 // then draw the line... 449 drawFirstPassShape(g2, pass, series, item, s.seriesPath); 450 } 451 // reset points vector 452 s.points = new ArrayList<>(); 453 } 454 } 455 456 private void solveTridiag(float[] sub, float[] diag, float[] sup, 457 float[] b, int n) { 458/* solve linear system with tridiagonal n by n matrix a 459 using Gaussian elimination *without* pivoting 460 where a(i,i-1) = sub[i] for 2<=i<=n 461 a(i,i) = diag[i] for 1<=i<=n 462 a(i,i+1) = sup[i] for 1<=i<=n-1 463 (the values sub[1], sup[n] are ignored) 464 right hand side vector b[1:n] is overwritten with solution 465 NOTE: 1...n is used in all arrays, 0 is unused */ 466 int i; 467/* factorization and forward substitution */ 468 for (i = 2; i <= n; i++) { 469 sub[i] /= diag[i - 1]; 470 diag[i] -= sub[i] * sup[i - 1]; 471 b[i] -= sub[i] * b[i - 1]; 472 } 473 b[n] /= diag[n]; 474 for (i = n - 1; i >= 1; i--) 475 b[i] = (b[i] - sup[i] * b[i + 1]) / diag[i]; 476 } 477 478 /** 479 * Tests this renderer for equality with an arbitrary object. 480 * 481 * @param obj the object ({@code null} permitted). 482 * 483 * @return A boolean. 484 */ 485 @Override 486 public boolean equals(Object obj) { 487 if (obj == this) { 488 return true; 489 } 490 if (!(obj instanceof XYSplineRenderer)) { 491 return false; 492 } 493 XYSplineRenderer that = (XYSplineRenderer) obj; 494 if (this.precision != that.precision) { 495 return false; 496 } 497 if (this.fillType != that.fillType) { 498 return false; 499 } 500 if (!Objects.equals(this.gradientPaintTransformer, that.gradientPaintTransformer)) { 501 return false; 502 } 503 return super.equals(obj); 504 } 505}