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