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