001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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-2011, 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 * 044 */ 045 046package org.jfree.chart.renderer.xy; 047 048import java.awt.Graphics2D; 049import java.awt.geom.Rectangle2D; 050import java.util.Vector; 051 052import org.jfree.chart.axis.ValueAxis; 053import org.jfree.chart.event.RendererChangeEvent; 054import org.jfree.chart.plot.PlotOrientation; 055import org.jfree.chart.plot.PlotRenderingInfo; 056import org.jfree.chart.plot.XYPlot; 057import org.jfree.data.xy.XYDataset; 058import org.jfree.ui.RectangleEdge; 059 060/** 061 * A renderer that connects data points with natural cubic splines and/or 062 * draws shapes at each data point. This renderer is designed for use with 063 * the {@link XYPlot} class. The example shown here is generated by the 064 * <code>XYSplineRendererDemo1.java</code> program included in the JFreeChart 065 * demo collection: 066 * <br><br> 067 * <img src="../../../../../images/XYSplineRendererSample.png" 068 * alt="XYSplineRendererSample.png" /> 069 * 070 * @since 1.0.7 071 */ 072public class XYSplineRenderer extends XYLineAndShapeRenderer { 073 074 /** 075 * To collect data points for later splining. 076 */ 077 private Vector points; 078 079 /** 080 * Resolution of splines (number of line segments between points) 081 */ 082 private int precision; 083 084 /** 085 * Creates a new instance with the 'precision' attribute defaulting to 086 * 5. 087 */ 088 public XYSplineRenderer() { 089 this(5); 090 } 091 092 /** 093 * Creates a new renderer with the specified precision. 094 * 095 * @param precision the number of points between data items. 096 */ 097 public XYSplineRenderer(int precision) { 098 super(); 099 if (precision <= 0) { 100 throw new IllegalArgumentException("Requires precision > 0."); 101 } 102 this.precision = precision; 103 } 104 105 /** 106 * Get the resolution of splines. 107 * 108 * @return Number of line segments between points. 109 * 110 * @see #setPrecision(int) 111 */ 112 public int getPrecision() { 113 return this.precision; 114 } 115 116 /** 117 * Set the resolution of splines and sends a {@link RendererChangeEvent} 118 * to all registered listeners. 119 * 120 * @param p number of line segments between points (must be > 0). 121 * 122 * @see #getPrecision() 123 */ 124 public void setPrecision(int p) { 125 if (p <= 0) { 126 throw new IllegalArgumentException("Requires p > 0."); 127 } 128 this.precision = p; 129 fireChangeEvent(); 130 } 131 132 /** 133 * Initialises the renderer. 134 * <P> 135 * This method will be called before the first item is rendered, giving the 136 * renderer an opportunity to initialise any state information it wants to 137 * maintain. The renderer can do nothing if it chooses. 138 * 139 * @param g2 the graphics device. 140 * @param dataArea the area inside the axes. 141 * @param plot the plot. 142 * @param data the data. 143 * @param info an optional info collection object to return data back to 144 * the caller. 145 * 146 * @return The renderer state. 147 */ 148 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 149 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 150 151 State state = (State) super.initialise(g2, dataArea, plot, data, info); 152 state.setProcessVisibleItemsOnly(false); 153 this.points = new Vector(); 154 setDrawSeriesLineAsPath(true); 155 return state; 156 } 157 158 /** 159 * Draws the item (first pass). This method draws the lines 160 * connecting the items. Instead of drawing separate lines, 161 * a GeneralPath is constructed and drawn at the end of 162 * the series painting. 163 * 164 * @param g2 the graphics device. 165 * @param state the renderer state. 166 * @param plot the plot (can be used to obtain standard color information 167 * etc). 168 * @param dataset the dataset. 169 * @param pass the pass. 170 * @param series the series index (zero-based). 171 * @param item the item index (zero-based). 172 * @param domainAxis the domain axis. 173 * @param rangeAxis the range axis. 174 * @param dataArea the area within which the data is being drawn. 175 */ 176 protected void drawPrimaryLineAsPath(XYItemRendererState state, 177 Graphics2D g2, XYPlot plot, XYDataset dataset, int pass, 178 int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis, 179 Rectangle2D dataArea) { 180 181 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 182 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 183 184 // get the data points 185 double x1 = dataset.getXValue(series, item); 186 double y1 = dataset.getYValue(series, item); 187 double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); 188 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); 189 190 // collect points 191 if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) { 192 ControlPoint p = new ControlPoint(plot.getOrientation() 193 == PlotOrientation.HORIZONTAL ? (float) transY1 194 : (float) transX1, plot.getOrientation() 195 == PlotOrientation.HORIZONTAL ? (float) transX1 196 : (float) transY1); 197 if (!this.points.contains(p)) { 198 this.points.add(p); 199 } 200 } 201 if (item == dataset.getItemCount(series) - 1) { 202 State s = (State) state; 203 // construct path 204 if (this.points.size() > 1) { 205 // we need at least two points to draw something 206 ControlPoint cp0 = (ControlPoint) this.points.get(0); 207 s.seriesPath.moveTo(cp0.x, cp0.y); 208 if (this.points.size() == 2) { 209 // we need at least 3 points to spline. Draw simple line 210 // for two points 211 ControlPoint cp1 = (ControlPoint) this.points.get(1); 212 s.seriesPath.lineTo(cp1.x, cp1.y); 213 } 214 else { 215 // construct spline 216 int np = this.points.size(); // number of points 217 float[] d = new float[np]; // Newton form coefficients 218 float[] x = new float[np]; // x-coordinates of nodes 219 float y; 220 float t; 221 float oldy = 0; 222 float oldt = 0; 223 224 float[] a = new float[np]; 225 float t1; 226 float t2; 227 float[] h = new float[np]; 228 229 for (int i = 0; i < np; i++) { 230 ControlPoint cpi = (ControlPoint) this.points.get(i); 231 x[i] = cpi.x; 232 d[i] = cpi.y; 233 } 234 235 for (int i = 1; i <= np - 1; i++) { 236 h[i] = x[i] - x[i - 1]; 237 } 238 float[] sub = new float[np - 1]; 239 float[] diag = new float[np - 1]; 240 float[] sup = new float[np - 1]; 241 242 for (int i = 1; i <= np - 2; i++) { 243 diag[i] = (h[i] + h[i + 1]) / 3; 244 sup[i] = h[i + 1] / 6; 245 sub[i] = h[i] / 6; 246 a[i] = (d[i + 1] - d[i]) / h[i + 1] 247 - (d[i] - d[i - 1]) / h[i]; 248 } 249 solveTridiag(sub, diag, sup, a, np - 2); 250 251 // note that a[0]=a[np-1]=0 252 // draw 253 oldt = x[0]; 254 oldy = d[0]; 255 s.seriesPath.moveTo(oldt, oldy); 256 for (int i = 1; i <= np - 1; i++) { 257 // loop over intervals between nodes 258 for (int j = 1; j <= this.precision; j++) { 259 t1 = (h[i] * j) / this.precision; 260 t2 = h[i] - t1; 261 y = ((-a[i - 1] / 6 * (t2 + h[i]) * t1 + d[i - 1]) 262 * t2 + (-a[i] / 6 * (t1 + h[i]) * t2 263 + d[i]) * t1) / h[i]; 264 t = x[i - 1] + t1; 265 s.seriesPath.lineTo(t, y); 266 } 267 } 268 } 269 // draw path 270 drawFirstPassShape(g2, pass, series, item, s.seriesPath); 271 } 272 273 // reset points vector 274 this.points = new Vector(); 275 } 276 } 277 278 private void solveTridiag(float[] sub, float[] diag, float[] sup, 279 float[] b, int n) { 280/* solve linear system with tridiagonal n by n matrix a 281 using Gaussian elimination *without* pivoting 282 where a(i,i-1) = sub[i] for 2<=i<=n 283 a(i,i) = diag[i] for 1<=i<=n 284 a(i,i+1) = sup[i] for 1<=i<=n-1 285 (the values sub[1], sup[n] are ignored) 286 right hand side vector b[1:n] is overwritten with solution 287 NOTE: 1...n is used in all arrays, 0 is unused */ 288 int i; 289/* factorization and forward substitution */ 290 for (i = 2; i <= n; i++) { 291 sub[i] = sub[i] / diag[i - 1]; 292 diag[i] = diag[i] - sub[i] * sup[i - 1]; 293 b[i] = b[i] - sub[i] * b[i - 1]; 294 } 295 b[n] = b[n] / diag[n]; 296 for (i = n - 1; i >= 1; i--) { 297 b[i] = (b[i] - sup[i] * b[i + 1]) / diag[i]; 298 } 299 } 300 301 /** 302 * Tests this renderer for equality with an arbitrary object. 303 * 304 * @param obj the object (<code>null</code> permitted). 305 * 306 * @return A boolean. 307 */ 308 public boolean equals(Object obj) { 309 if (obj == this) { 310 return true; 311 } 312 if (!(obj instanceof XYSplineRenderer)) { 313 return false; 314 } 315 XYSplineRenderer that = (XYSplineRenderer) obj; 316 if (this.precision != that.precision) { 317 return false; 318 } 319 return super.equals(obj); 320 } 321 322 /** 323 * Represents a control point. 324 */ 325 class ControlPoint { 326 327 /** The x-coordinate. */ 328 public float x; 329 330 /** The y-coordinate. */ 331 public float y; 332 333 /** 334 * Creates a new control point. 335 * 336 * @param x the x-coordinate. 337 * @param y the y-coordinate. 338 */ 339 public ControlPoint(float x, float y) { 340 this.x = x; 341 this.y = y; 342 } 343 344 /** 345 * Tests this point for equality with an arbitrary object. 346 * 347 * @param obj the object (<code>null</code> permitted. 348 * 349 * @return A boolean. 350 */ 351 public boolean equals(Object obj) { 352 if (obj == this) { 353 return true; 354 } 355 if (!(obj instanceof ControlPoint)) { 356 return false; 357 } 358 ControlPoint that = (ControlPoint) obj; 359 if (this.x != that.x) { 360 return false; 361 } 362 /*&& y == ((ControlPoint) obj).y;*/ 363 return true; 364 } 365 366 } 367}