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