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