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