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