001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2020, 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-2016, 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 */
038
039package org.jfree.chart.renderer.xy;
040
041import java.awt.GradientPaint;
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.geom.GeneralPath;
045import java.awt.geom.Point2D;
046import java.awt.geom.Rectangle2D;
047import java.util.ArrayList;
048import java.util.List;
049
050import org.jfree.chart.axis.ValueAxis;
051import org.jfree.chart.event.RendererChangeEvent;
052import org.jfree.chart.plot.PlotOrientation;
053import org.jfree.chart.plot.PlotRenderingInfo;
054import org.jfree.chart.plot.XYPlot;
055import org.jfree.chart.ui.GradientPaintTransformer;
056import org.jfree.chart.ui.RectangleEdge;
057import org.jfree.chart.ui.StandardGradientPaintTransformer;
058import org.jfree.chart.util.ObjectUtils;
059import org.jfree.chart.util.Args;
060import org.jfree.data.xy.XYDataset;
061
062/**
063 * A renderer that connects data points with natural cubic splines and/or
064 * draws shapes at each data point.  This renderer is designed for use with
065 * the {@link XYPlot} class. The example shown here is generated by the
066 * {@code XYSplineRendererDemo1.java} program included in the JFreeChart
067 * demo collection:
068 * <br><br>
069 * <img src="doc-files/XYSplineRendererSample.png" alt="XYSplineRendererSample.png">
070 *
071 * @since 1.0.7
072 */
073public class XYSplineRenderer extends XYLineAndShapeRenderer {
074
075    /**
076     * An enumeration of the fill types for the renderer.
077     * 
078     * @since 1.0.17
079     */
080    public static enum FillType {
081       
082        /** No fill. */
083        NONE,
084        
085        /** Fill towards zero. */
086        TO_ZERO,
087
088        /** Fill to lower bound. */
089        TO_LOWER_BOUND,
090        
091        /** Fill to upper bound. */
092        TO_UPPER_BOUND
093    }
094    
095    /**
096     * Represents state information that applies to a single rendering of
097     * a chart.
098     */
099    public static class XYSplineState extends State {
100        
101        /** The area to fill under the curve. */
102        public GeneralPath fillArea;
103        
104        /** The points. */
105        public List<Point2D> points;
106        
107        /**
108         * Creates a new state instance.
109         * 
110         * @param info  the plot rendering info. 
111         */
112        public XYSplineState(PlotRenderingInfo info) {
113            super(info);
114            this.fillArea = new GeneralPath();
115            this.points = new ArrayList<Point2D>();
116        }
117    }
118    
119    /**
120     * Resolution of splines (number of line segments between points)
121     */
122    private int precision;
123
124    /**
125     * A flag that can be set to specify 
126     * to fill the area under the spline.
127     */
128    private FillType fillType;
129
130    private GradientPaintTransformer gradientPaintTransformer;
131    
132    /**
133     * Creates a new instance with the precision attribute defaulting to 5 
134     * and no fill of the area 'under' the spline.
135     */
136    public XYSplineRenderer() {
137        this(5, FillType.NONE);
138    }
139
140    /**
141     * Creates a new renderer with the specified precision 
142     * and no fill of the area 'under' (between '0' and) the spline.
143     *
144     * @param precision  the number of points between data items.
145     */
146    public XYSplineRenderer(int precision) {
147        this(precision, FillType.NONE);
148    }
149
150    /**
151     * Creates a new renderer with the specified precision
152     * and specified fill of the area 'under' (between '0' and) the spline.
153     *
154     * @param precision  the number of points between data items.
155     * @param fillType  the type of fill beneath the curve ({@code null} 
156     *     not permitted).
157     * 
158     * @since 1.0.17
159     */
160    public XYSplineRenderer(int precision, FillType fillType) {
161        super();
162        if (precision <= 0) {
163            throw new IllegalArgumentException("Requires precision > 0.");
164        }
165        Args.nullNotPermitted(fillType, "fillType");
166        this.precision = precision;
167        this.fillType = fillType;
168        this.gradientPaintTransformer = new StandardGradientPaintTransformer();
169    }
170
171    /**
172     * Returns the number of line segments used to approximate the spline
173     * curve between data points.
174     *
175     * @return The number of line segments.
176     *
177     * @see #setPrecision(int)
178     */
179    public int getPrecision() {
180        return this.precision;
181    }
182
183    /**
184     * Set the resolution of splines and sends a {@link RendererChangeEvent}
185     * to all registered listeners.
186     *
187     * @param p  number of line segments between points (must be &gt; 0).
188     *
189     * @see #getPrecision()
190     */
191    public void setPrecision(int p) {
192        if (p <= 0) {
193            throw new IllegalArgumentException("Requires p > 0.");
194        }
195        this.precision = p;
196        fireChangeEvent();
197    }
198
199    /**
200     * Returns the type of fill that the renderer draws beneath the curve.
201     *
202     * @return The type of fill (never {@code null}).
203     *
204     * @see #setFillType(FillType) 
205     * 
206     * @since 1.0.17
207     */
208    public FillType getFillType() {
209        return this.fillType;
210    }
211
212    /**
213     * Set the fill type and sends a {@link RendererChangeEvent}
214     * to all registered listeners.
215     *
216     * @param fillType   the fill type ({@code null} not permitted).
217     *
218     * @see #getFillType()
219     * 
220     * @since 1.0.17
221     */
222    public void setFillType(FillType fillType) {
223        this.fillType = fillType;
224        fireChangeEvent();
225    }
226
227    /**
228     * Returns the gradient paint transformer, or {@code null}.
229     * 
230     * @return The gradient paint transformer (possibly {@code null}).
231     * 
232     * @since 1.0.17
233     */
234    public GradientPaintTransformer getGradientPaintTransformer() {
235        return this.gradientPaintTransformer;
236    }
237    
238    /**
239     * Sets the gradient paint transformer and sends a 
240     * {@link RendererChangeEvent} to all registered listeners.
241     * 
242     * @param gpt  the transformer ({@code null} permitted).
243     * 
244     * @since 1.0.17
245     */
246    public void setGradientPaintTransformer(GradientPaintTransformer gpt) {
247        this.gradientPaintTransformer = gpt;
248        fireChangeEvent();
249    }
250    
251    /**
252     * Initialises the renderer.
253     * <P>
254     * This method will be called before the first item is rendered, giving the
255     * renderer an opportunity to initialise any state information it wants to
256     * maintain.  The renderer can do nothing if it chooses.
257     *
258     * @param g2  the graphics device.
259     * @param dataArea  the area inside the axes.
260     * @param plot  the plot.
261     * @param data  the data.
262     * @param info  an optional info collection object to return data back to
263     *              the caller.
264     *
265     * @return The renderer state.
266     */
267    @Override
268    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
269            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
270
271        setDrawSeriesLineAsPath(true);
272        XYSplineState state = new XYSplineState(info);
273        state.setProcessVisibleItemsOnly(false);
274        return state;
275    }
276
277    /**
278     * Draws the item (first pass). This method draws the lines
279     * connecting the items. Instead of drawing separate lines,
280     * a GeneralPath is constructed and drawn at the end of
281     * the series painting.
282     *
283     * @param g2  the graphics device.
284     * @param state  the renderer state.
285     * @param plot  the plot (can be used to obtain standard color information
286     *              etc).
287     * @param dataset  the dataset.
288     * @param pass  the pass.
289     * @param series  the series index (zero-based).
290     * @param item  the item index (zero-based).
291     * @param xAxis  the domain axis.
292     * @param yAxis  the range axis.
293     * @param dataArea  the area within which the data is being drawn.
294     */
295    @Override
296    protected void drawPrimaryLineAsPath(XYItemRendererState state,
297            Graphics2D g2, XYPlot plot, XYDataset dataset, int pass,
298            int series, int item, ValueAxis xAxis, ValueAxis yAxis,
299            Rectangle2D dataArea) {
300
301        XYSplineState s = (XYSplineState) state;
302        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
303        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
304
305        // get the data points
306        double x1 = dataset.getXValue(series, item);
307        double y1 = dataset.getYValue(series, item);
308        double transX1 = xAxis.valueToJava2D(x1, dataArea, xAxisLocation);
309        double transY1 = yAxis.valueToJava2D(y1, dataArea, yAxisLocation);
310
311        // Collect points
312        if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
313            Point2D p = plot.getOrientation() == PlotOrientation.HORIZONTAL 
314                ? new Point2D.Float((float) transY1, (float) transX1) 
315                : new Point2D.Float((float) transX1, (float) transY1);
316            if (!s.points.contains(p))
317                s.points.add(p);
318        }
319        
320        if (item == dataset.getItemCount(series) - 1) {     // construct path
321            if (s.points.size() > 1) {
322                Point2D origin;
323                if (this.fillType == FillType.TO_ZERO) {
324                    float xz = (float) xAxis.valueToJava2D(0, dataArea, 
325                            yAxisLocation);
326                    float yz = (float) yAxis.valueToJava2D(0, dataArea, 
327                            yAxisLocation);
328                    origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
329                            ? new Point2D.Float(yz, xz) 
330                            : new Point2D.Float(xz, yz);
331                } else if (this.fillType == FillType.TO_LOWER_BOUND) {
332                    float xlb = (float) xAxis.valueToJava2D(
333                            xAxis.getLowerBound(), dataArea, xAxisLocation);
334                    float ylb = (float) yAxis.valueToJava2D(
335                            yAxis.getLowerBound(), dataArea, yAxisLocation);
336                    origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
337                            ? new Point2D.Float(ylb, xlb) 
338                            : new Point2D.Float(xlb, ylb);
339                } else {// fillType == TO_UPPER_BOUND
340                    float xub = (float) xAxis.valueToJava2D(
341                            xAxis.getUpperBound(), dataArea, xAxisLocation);
342                    float yub = (float) yAxis.valueToJava2D(
343                            yAxis.getUpperBound(), dataArea, yAxisLocation);
344                    origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
345                            ? new Point2D.Float(yub, xub)
346                            : new Point2D.Float(xub, yub);
347                }
348                
349                // we need at least two points to draw something
350                Point2D cp0 = s.points.get(0);
351                s.seriesPath.moveTo(cp0.getX(), cp0.getY());
352                if (this.fillType != FillType.NONE) {
353                    if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
354                        s.fillArea.moveTo(origin.getX(), cp0.getY());
355                    } else {
356                        s.fillArea.moveTo(cp0.getX(), origin.getY());
357                    }
358                    s.fillArea.lineTo(cp0.getX(), cp0.getY());
359                }
360                if (s.points.size() == 2) {
361                    // we need at least 3 points to spline. Draw simple line
362                    // for two points
363                    Point2D cp1 = s.points.get(1);
364                    if (this.fillType != FillType.NONE) {
365                        s.fillArea.lineTo(cp1.getX(), cp1.getY());
366                        s.fillArea.lineTo(cp1.getX(), origin.getY());
367                        s.fillArea.closePath();
368                    }
369                    s.seriesPath.lineTo(cp1.getX(), cp1.getY());
370                } else {
371                    // construct spline
372                    int np = s.points.size(); // number of points
373                    float[] d = new float[np]; // Newton form coefficients
374                    float[] x = new float[np]; // x-coordinates of nodes
375                    float y, oldy;
376                    float t, oldt;
377
378                    float[] a = new float[np];
379                    float t1;
380                    float t2;
381                    float[] h = new float[np];
382
383                    for (int i = 0; i < np; i++) {
384                        Point2D.Float cpi = (Point2D.Float) s.points.get(i);
385                        x[i] = cpi.x;
386                        d[i] = cpi.y;
387                    }
388
389                    for (int i = 1; i <= np - 1; i++)
390                        h[i] = x[i] - x[i - 1];
391
392                    float[] sub = new float[np - 1];
393                    float[] diag = new float[np - 1];
394                    float[] sup = new float[np - 1];
395
396                    for (int i = 1; i <= np - 2; i++) {
397                        diag[i] = (h[i] + h[i + 1]) / 3;
398                        sup[i] = h[i + 1] / 6;
399                        sub[i] = h[i] / 6;
400                        a[i] = (d[i + 1] - d[i]) / h[i + 1]
401                                   - (d[i] - d[i - 1]) / h[i];
402                    }
403                    solveTridiag(sub, diag, sup, a, np - 2);
404
405                    // note that a[0]=a[np-1]=0
406                    oldt = x[0];
407                    oldy = d[0];
408                    for (int i = 1; i <= np - 1; i++) {
409                        // loop over intervals between nodes
410                        for (int j = 1; j <= this.precision; j++) {
411                            t1 = (h[i] * j) / this.precision;
412                            t2 = h[i] - t1;
413                            y = ((-a[i - 1] / 6 * (t2 + h[i]) * t1 + d[i - 1])
414                                    * t2 + (-a[i] / 6 * (t1 + h[i]) * t2
415                                    + d[i]) * t1) / h[i];
416                            t = x[i - 1] + t1;
417                            s.seriesPath.lineTo(t, y);
418                            if (this.fillType != FillType.NONE) {
419                                s.fillArea.lineTo(t, y);
420                            }
421                        }
422                    }
423                }
424                // Add last point @ y=0 for fillPath and close path
425                if (this.fillType != FillType.NONE) {
426                    if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
427                        s.fillArea.lineTo(origin.getX(), s.points.get(
428                                s.points.size() - 1).getY());
429                    } else {
430                        s.fillArea.lineTo(s.points.get(
431                                s.points.size() - 1).getX(), origin.getY());
432                    }
433                    s.fillArea.closePath();
434                }
435
436                // fill under the curve...
437                if (this.fillType != FillType.NONE) {
438                    Paint fp = getSeriesFillPaint(series);
439                    if (this.gradientPaintTransformer != null 
440                            && fp instanceof GradientPaint) {
441                        GradientPaint gp = this.gradientPaintTransformer
442                                .transform((GradientPaint) fp, s.fillArea);
443                        g2.setPaint(gp);
444                    } else {
445                        g2.setPaint(fp);                        
446                    }
447                    g2.fill(s.fillArea);
448                    s.fillArea.reset();
449                }
450                // then draw the line...
451                drawFirstPassShape(g2, pass, series, item, s.seriesPath);
452            }
453            // reset points vector
454            s.points = new ArrayList<Point2D>();
455        }
456    }
457    
458    private void solveTridiag(float[] sub, float[] diag, float[] sup,
459            float[] b, int n) {
460/*      solve linear system with tridiagonal n by n matrix a
461        using Gaussian elimination *without* pivoting
462        where   a(i,i-1) = sub[i]  for 2<=i<=n
463        a(i,i)   = diag[i] for 1<=i<=n
464        a(i,i+1) = sup[i]  for 1<=i<=n-1
465        (the values sub[1], sup[n] are ignored)
466        right hand side vector b[1:n] is overwritten with solution
467        NOTE: 1...n is used in all arrays, 0 is unused */
468        int i;
469/*      factorization and forward substitution */
470        for (i = 2; i <= n; i++) {
471            sub[i] /= diag[i - 1];
472            diag[i] -= sub[i] * sup[i - 1];
473            b[i] -= sub[i] * b[i - 1];
474        }
475        b[n] /= diag[n];
476        for (i = n - 1; i >= 1; i--)
477            b[i] = (b[i] - sup[i] * b[i + 1]) / diag[i];
478    }
479
480    /**
481     * Tests this renderer for equality with an arbitrary object.
482     *
483     * @param obj  the object ({@code null} permitted).
484     *
485     * @return A boolean.
486     */
487    @Override
488    public boolean equals(Object obj) {
489        if (obj == this) {
490            return true;
491        }
492        if (!(obj instanceof XYSplineRenderer)) {
493            return false;
494        }
495        XYSplineRenderer that = (XYSplineRenderer) obj;
496        if (this.precision != that.precision) {
497            return false;
498        }
499        if (this.fillType != that.fillType) {
500            return false;
501        }
502        if (!ObjectUtils.equal(this.gradientPaintTransformer, 
503                that.gradientPaintTransformer)) {
504            return false;
505        }
506        return super.equals(obj);
507    }
508}