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