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 }