/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
*
* Project Info: http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* ---------------------
* XYSplineRenderer.java
* ---------------------
* (C) Copyright 2007-2011, by Klaus Rheinwald and Contributors.
*
* Original Author: Klaus Rheinwald;
* Contributor(s): Tobias von Petersdorff (tvp@math.umd.edu,
* http://www.wam.umd.edu/~petersd/);
* David Gilbert (for Object Refinery Limited);
*
* Changes:
* --------
* 25-Jul-2007 : Version 1, contributed by Klaus Rheinwald (DG);
* 03-Aug-2007 : Added new constructor (KR);
* 25-Oct-2007 : Prevent duplicate control points (KR);
* 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG);
*
*/
package org.jfree.chart.renderer.xy;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.Vector;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.xy.XYDataset;
import org.jfree.ui.RectangleEdge;
/**
* A renderer that connects data points with natural cubic splines and/or
* draws shapes at each data point. This renderer is designed for use with
* the {@link XYPlot} class. The example shown here is generated by the
* <code>XYSplineRendererDemo1.java</code> program included in the JFreeChart
* demo collection:
* <br><br>
* <img src="../../../../../images/XYSplineRendererSample.png"
* alt="XYSplineRendererSample.png" />
*
* @since 1.0.7
*/
public class XYSplineRenderer extends XYLineAndShapeRenderer {
/**
* To collect data points for later splining.
*/
private Vector points;
/**
* Resolution of splines (number of line segments between points)
*/
private int precision;
/**
* Creates a new instance with the 'precision' attribute defaulting to
* 5.
*/
public XYSplineRenderer() {
this(5);
}
/**
* Creates a new renderer with the specified precision.
*
* @param precision the number of points between data items.
*/
public XYSplineRenderer(int precision) {
super();
if (precision <= 0) {
throw new IllegalArgumentException("Requires precision > 0.");
}
this.precision = precision;
}
/**
* Get the resolution of splines.
*
* @return Number of line segments between points.
*
* @see #setPrecision(int)
*/
public int getPrecision() {
return this.precision;
}
/**
* Set the resolution of splines and sends a {@link RendererChangeEvent}
* to all registered listeners.
*
* @param p number of line segments between points (must be > 0).
*
* @see #getPrecision()
*/
public void setPrecision(int p) {
if (p <= 0) {
throw new IllegalArgumentException("Requires p > 0.");
}
this.precision = p;
fireChangeEvent();
}
/**
* Initialises the renderer.
* <P>
* This method will be called before the first item is rendered, giving the
* renderer an opportunity to initialise any state information it wants to
* maintain. The renderer can do nothing if it chooses.
*
* @param g2 the graphics device.
* @param dataArea the area inside the axes.
* @param plot the plot.
* @param data the data.
* @param info an optional info collection object to return data back to
* the caller.
*
* @return The renderer state.
*/
public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
XYPlot plot, XYDataset data, PlotRenderingInfo info) {
State state = (State) super.initialise(g2, dataArea, plot, data, info);
state.setProcessVisibleItemsOnly(false);
this.points = new Vector();
setDrawSeriesLineAsPath(true);
return state;
}
/**
* Draws the item (first pass). This method draws the lines
* connecting the items. Instead of drawing separate lines,
* a GeneralPath is constructed and drawn at the end of
* the series painting.
*
* @param g2 the graphics device.
* @param state the renderer state.
* @param plot the plot (can be used to obtain standard color information
* etc).
* @param dataset the dataset.
* @param pass the pass.
* @param series the series index (zero-based).
* @param item the item index (zero-based).
* @param domainAxis the domain axis.
* @param rangeAxis the range axis.
* @param dataArea the area within which the data is being drawn.
*/
protected void drawPrimaryLineAsPath(XYItemRendererState state,
Graphics2D g2, XYPlot plot, XYDataset dataset, int pass,
int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis,
Rectangle2D dataArea) {
RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
// get the data points
double x1 = dataset.getXValue(series, item);
double y1 = dataset.getYValue(series, item);
double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
// collect points
if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
ControlPoint p = new ControlPoint(plot.getOrientation()
== PlotOrientation.HORIZONTAL ? (float) transY1
: (float) transX1, plot.getOrientation()
== PlotOrientation.HORIZONTAL ? (float) transX1
: (float) transY1);
if (!this.points.contains(p)) {
this.points.add(p);
}
}
if (item == dataset.getItemCount(series) - 1) {
State s = (State) state;
// construct path
if (this.points.size() > 1) {
// we need at least two points to draw something
ControlPoint cp0 = (ControlPoint) this.points.get(0);
s.seriesPath.moveTo(cp0.x, cp0.y);
if (this.points.size() == 2) {
// we need at least 3 points to spline. Draw simple line
// for two points
ControlPoint cp1 = (ControlPoint) this.points.get(1);
s.seriesPath.lineTo(cp1.x, cp1.y);
}
else {
// construct spline
int np = this.points.size(); // number of points
float[] d = new float[np]; // Newton form coefficients
float[] x = new float[np]; // x-coordinates of nodes
float y;
float t;
float oldy = 0;
float oldt = 0;
float[] a = new float[np];
float t1;
float t2;
float[] h = new float[np];
for (int i = 0; i < np; i++) {
ControlPoint cpi = (ControlPoint) this.points.get(i);
x[i] = cpi.x;
d[i] = cpi.y;
}
for (int i = 1; i <= np - 1; i++) {
h[i] = x[i] - x[i - 1];
}
float[] sub = new float[np - 1];
float[] diag = new float[np - 1];
float[] sup = new float[np - 1];
for (int i = 1; i <= np - 2; i++) {
diag[i] = (h[i] + h[i + 1]) / 3;
sup[i] = h[i + 1] / 6;
sub[i] = h[i] / 6;
a[i] = (d[i + 1] - d[i]) / h[i + 1]
- (d[i] - d[i - 1]) / h[i];
}
solveTridiag(sub, diag, sup, a, np - 2);
// note that a[0]=a[np-1]=0
// draw
oldt = x[0];
oldy = d[0];
s.seriesPath.moveTo(oldt, oldy);
for (int i = 1; i <= np - 1; i++) {
// loop over intervals between nodes
for (int j = 1; j <= this.precision; j++) {
t1 = (h[i] * j) / this.precision;
t2 = h[i] - t1;
y = ((-a[i - 1] / 6 * (t2 + h[i]) * t1 + d[i - 1])
* t2 + (-a[i] / 6 * (t1 + h[i]) * t2
+ d[i]) * t1) / h[i];
t = x[i - 1] + t1;
s.seriesPath.lineTo(t, y);
}
}
}
// draw path
drawFirstPassShape(g2, pass, series, item, s.seriesPath);
}
// reset points vector
this.points = new Vector();
}
}
private void solveTridiag(float[] sub, float[] diag, float[] sup,
float[] b, int n) {
/* solve linear system with tridiagonal n by n matrix a
using Gaussian elimination *without* pivoting
where a(i,i-1) = sub[i] for 2<=i<=n
a(i,i) = diag[i] for 1<=i<=n
a(i,i+1) = sup[i] for 1<=i<=n-1
(the values sub[1], sup[n] are ignored)
right hand side vector b[1:n] is overwritten with solution
NOTE: 1...n is used in all arrays, 0 is unused */
int i;
/* factorization and forward substitution */
for (i = 2; i <= n; i++) {
sub[i] = sub[i] / diag[i - 1];
diag[i] = diag[i] - sub[i] * sup[i - 1];
b[i] = b[i] - sub[i] * b[i - 1];
}
b[n] = b[n] / diag[n];
for (i = n - 1; i >= 1; i--) {
b[i] = (b[i] - sup[i] * b[i + 1]) / diag[i];
}
}
/**
* Tests this renderer for equality with an arbitrary object.
*
* @param obj the object (<code>null</code> permitted).
*
* @return A boolean.
*/
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof XYSplineRenderer)) {
return false;
}
XYSplineRenderer that = (XYSplineRenderer) obj;
if (this.precision != that.precision) {
return false;
}
return super.equals(obj);
}
/**
* Represents a control point.
*/
class ControlPoint {
/** The x-coordinate. */
public float x;
/** The y-coordinate. */
public float y;
/**
* Creates a new control point.
*
* @param x the x-coordinate.
* @param y the y-coordinate.
*/
public ControlPoint(float x, float y) {
this.x = x;
this.y = y;
}
/**
* Tests this point for equality with an arbitrary object.
*
* @param obj the object (<code>null</code> permitted.
*
* @return A boolean.
*/
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof ControlPoint)) {
return false;
}
ControlPoint that = (ControlPoint) obj;
if (this.x != that.x) {
return false;
}
/*&& y == ((ControlPoint) obj).y;*/
return true;
}
}
}