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
045package org.jfree.chart.renderer.xy;
046
047import java.awt.Graphics2D;
048import java.awt.geom.Rectangle2D;
049import java.util.Vector;
050
051import org.jfree.chart.axis.ValueAxis;
052import org.jfree.chart.event.RendererChangeEvent;
053import org.jfree.chart.plot.PlotOrientation;
054import org.jfree.chart.plot.PlotRenderingInfo;
055import org.jfree.chart.plot.XYPlot;
056import org.jfree.data.xy.XYDataset;
057import 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 */
071public 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}