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