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