+ *
+ * @since 1.0.7
+ */
+public class XYSplineRenderer extends XYLineAndShapeRenderer {
+
+ /**
+ * An enumeration of the fill types for the renderer.
+ *
+ * @since 1.0.17
+ */
+ public static enum FillType {
+
+ /** No fill. */
+ NONE,
+
+ /** Fill down to zero. */
+ TO_ZERO,
+
+ /** Fill to the lower bound. */
+ TO_LOWER_BOUND,
+
+ /** Fill to the upper bound. */
+ TO_UPPER_BOUND
+ }
+
+ /**
+ * Represents state information that applies to a single rendering of
+ * a chart.
+ */
+ public static class XYSplineState extends State {
+
+ /** The area to fill under the curve. */
+ public GeneralPath fillArea;
+
+ /** The points. */
+ public List
+ * 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.
+ */
+ @Override
+ public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
+ XYPlot plot, XYDataset data, PlotRenderingInfo info) {
+
+ setDrawSeriesLineAsPath(true);
+ XYSplineState state = new XYSplineState(info);
+ state.setProcessVisibleItemsOnly(false);
+ 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 xAxis the domain axis.
+ * @param yAxis the range axis.
+ * @param dataArea the area within which the data is being drawn.
+ */
+ @Override
+ protected void drawPrimaryLineAsPath(XYItemRendererState state,
+ Graphics2D g2, XYPlot plot, XYDataset dataset, int pass,
+ int series, int item, ValueAxis xAxis, ValueAxis yAxis,
+ Rectangle2D dataArea) {
+
+ XYSplineState s = (XYSplineState) state;
+ 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 = xAxis.valueToJava2D(x1, dataArea, xAxisLocation);
+ double transY1 = yAxis.valueToJava2D(y1, dataArea, yAxisLocation);
+
+ // Collect points
+ if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
+ Point2D p = plot.getOrientation() == PlotOrientation.HORIZONTAL
+ ? new Point2D.Float((float) transY1, (float) transX1)
+ : new Point2D.Float((float) transX1, (float) transY1);
+ if (!s.points.contains(p))
+ s.points.add(p);
+ }
+
+ if (item == dataset.getItemCount(series) - 1) { // construct path
+ if (s.points.size() > 1) {
+ Point2D origin;
+ if (this.fillType == FillType.TO_ZERO) {
+ float xz = (float) xAxis.valueToJava2D(0, dataArea,
+ yAxisLocation);
+ float yz = (float) yAxis.valueToJava2D(0, dataArea,
+ yAxisLocation);
+ origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
+ ? new Point2D.Float(yz, xz)
+ : new Point2D.Float(xz, yz);
+ } else if (this.fillType == FillType.TO_LOWER_BOUND) {
+ float xlb = (float) xAxis.valueToJava2D(
+ xAxis.getLowerBound(), dataArea, xAxisLocation);
+ float ylb = (float) yAxis.valueToJava2D(
+ yAxis.getLowerBound(), dataArea, yAxisLocation);
+ origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
+ ? new Point2D.Float(ylb, xlb)
+ : new Point2D.Float(xlb, ylb);
+ } else {// fillType == TO_UPPER_BOUND
+ float xub = (float) xAxis.valueToJava2D(
+ xAxis.getUpperBound(), dataArea, xAxisLocation);
+ float yub = (float) yAxis.valueToJava2D(
+ yAxis.getUpperBound(), dataArea, yAxisLocation);
+ origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
+ ? new Point2D.Float(yub, xub)
+ : new Point2D.Float(xub, yub);
+ }
+
+ // we need at least two points to draw something
+ Point2D cp0 = s.points.get(0);
+ s.seriesPath.moveTo(cp0.getX(), cp0.getY());
+ if (this.fillType != FillType.NONE) {
+ if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
+ s.fillArea.moveTo(origin.getX(), cp0.getY());
+ } else {
+ s.fillArea.moveTo(cp0.getX(), origin.getY());
+ }
+ s.fillArea.lineTo(cp0.getX(), cp0.getY());
+ }
+ if (s.points.size() == 2) {
+ // we need at least 3 points to spline. Draw simple line
+ // for two points
+ Point2D cp1 = s.points.get(1);
+ if (this.fillType != FillType.NONE) {
+ s.fillArea.lineTo(cp1.getX(), cp1.getY());
+ s.fillArea.lineTo(cp1.getX(), origin.getY());
+ s.fillArea.closePath();
+ }
+ s.seriesPath.lineTo(cp1.getX(), cp1.getY());
+ } else {
+ // construct spline
+ int np = s.points.size(); // number of points
+ float[] d = new float[np]; // Newton form coefficients
+ float[] x = new float[np]; // x-coordinates of nodes
+ float y, oldy;
+ float t, oldt;
+
+ float[] a = new float[np];
+ float t1;
+ float t2;
+ float[] h = new float[np];
+
+ for (int i = 0; i < np; i++) {
+ Point2D.Float cpi = (Point2D.Float) s.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
+ oldt = x[0];
+ oldy = d[0];
+ 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);
+ if (this.fillType != FillType.NONE) {
+ s.fillArea.lineTo(t, y);
+ }
+ }
+ }
+ }
+ // begin fill the path, the range of y should be set first
+ NumberAxis range = plot.getRangeAxisEdge();
+ range.setRange(0, range.getUpperBound());
+
+ // Add last point @ y=0 for fillPath and close path
+ if (this.fillType != FillType.NONE) {
+ if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
+ s.fillArea.lineTo(origin.getX(), s.points.get(
+ s.points.size() - 1).getY());
+ } else {
+ s.fillArea.lineTo(s.points.get(
+ s.points.size() - 1).getX(), origin.getY());
+ }
+ s.fillArea.closePath();
+ }
+
+ // fill under the curve...
+ if (this.fillType != FillType.NONE) {
+ Paint fp = getSeriesFillPaint(series);
+ if (this.gradientPaintTransformer != null
+ && fp instanceof GradientPaint) {
+ GradientPaint gp = this.gradientPaintTransformer
+ .transform((GradientPaint) fp, s.fillArea);
+ g2.setPaint(gp);
+ } else {
+ g2.setPaint(fp);
+ }
+ g2.fill(s.fillArea);
+ s.fillArea.reset();
+ }
+ // then draw the line...
+ drawFirstPassShape(g2, pass, series, item, s.seriesPath);
+ }
+ // reset points vector
+ s.points = new ArrayList<>();
+ }
+ }
+
+ 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] /= diag[i - 1];
+ diag[i] -= sub[i] * sup[i - 1];
+ b[i] -= sub[i] * b[i - 1];
+ }
+ 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} permitted).
+ *
+ * @return A boolean.
+ */
+ @Override
+ 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;
+ }
+ if (this.fillType != that.fillType) {
+ return false;
+ }
+ if (!Objects.equals(this.gradientPaintTransformer, that.gradientPaintTransformer)) {
+ return false;
+ }
+ return super.equals(obj);
+ }
+}
diff --git a/src/main/java/org/jfree/chart/axis/LogAxis.java b/src/main/java/org/jfree/chart/axis/LogAxis.java
index 57b545773f..b1e5cd6c34 100644
--- a/src/main/java/org/jfree/chart/axis/LogAxis.java
+++ b/src/main/java/org/jfree/chart/axis/LogAxis.java
@@ -871,6 +871,19 @@ protected double estimateMaximumTickLabelWidth(Graphics2D g2,
return result;
}
+ /**
+ * Set range for the log plot
+ * @param range the new range for the plot, adjusted by missing zero values
+ */
+ @Override
+ public void setRange(Range range){
+ super.setRange(range);
+ double lower = range.getLowerBound();
+ if (lower < 0.0){
+ setLowerBound(smallestValue);
+ }
+ }
+
/**
* Zooms in on the current range.
*
diff --git a/src/main/java/org/jfree/chart/renderer/xy/XYSplineRenderer.java b/src/main/java/org/jfree/chart/renderer/xy/XYSplineRenderer.java
index 64df374b7f..6c800b34f9 100644
--- a/src/main/java/org/jfree/chart/renderer/xy/XYSplineRenderer.java
+++ b/src/main/java/org/jfree/chart/renderer/xy/XYSplineRenderer.java
@@ -48,6 +48,7 @@
import java.util.List;
import java.util.Objects;
+import org.jfree.chart.axis.CyclicNumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.plot.PlotOrientation;
@@ -421,6 +422,10 @@ protected void drawPrimaryLineAsPath(XYItemRendererState state,
}
}
}
+ // begin fill the path, the range of y should be set first
+ CyclicNumberAxis range = plot.getRangeAxisEdge();
+ range.setRange(0, range.getUpperBound());
+
// Add last point @ y=0 for fillPath and close path
if (this.fillType != FillType.NONE) {
if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
diff --git a/src/test/java/org/jfree/chart/axis/LogAxisBoundTest.java b/src/test/java/org/jfree/chart/axis/LogAxisBoundTest.java
new file mode 100644
index 0000000000..655327363b
--- /dev/null
+++ b/src/test/java/org/jfree/chart/axis/LogAxisBoundTest.java
@@ -0,0 +1,192 @@
+/* ===========================================================
+ * JFreeChart : a free chart library for the Java(tm) platform
+ * ===========================================================
+ *
+ * (C) Copyright 2000-2020, 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.]
+ *
+ * ----------------
+ * LogAxisBoundTest.java
+ * ----------------
+ * (C) Copyright 2007-2020, by Object Refinery Limited and Contributors.
+ *
+ * Original Author: David Gilbert (for Object Refinery Limited);
+ * Contributor(s): Zhiyue Xia created this test;
+ */
+
+package org.jfree.chart.axis;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.TestUtils;
+import org.jfree.chart.api.RectangleEdge;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+import org.junit.jupiter.api.Test;
+
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for the {@link LogAxis} class.
+ */
+public class LogAxisBoundTest {
+
+ /**
+ * Confirm that the equals method can distinguish all the required fields.
+ */
+ @Test
+ public void testEquals() {
+ LogAxis a1 = new LogAxis("Test");
+ LogAxis a2 = new LogAxis("Test");
+ assertTrue(a1.equals(a2));
+
+ a1.setBase(2.0);
+ assertFalse(a1.equals(a2));
+ a2.setBase(2.0);
+ assertTrue(a1.equals(a2));
+
+ a1.setSmallestValue(0.1);
+ assertFalse(a1.equals(a2));
+ a2.setSmallestValue(0.1);
+ assertTrue(a1.equals(a2));
+
+ a1.setMinorTickCount(8);
+ assertFalse(a1.equals(a2));
+ a2.setMinorTickCount(8);
+ assertTrue(a1.equals(a2));
+ }
+
+ private static final double EPSILON = 0.0000001;
+
+ /**
+ * Test the translation of Java2D values to data values.
+ */
+ @Test
+ public void testTranslateJava2DToValue() {
+ LogAxis axis = new LogAxis();
+ axis.setRange(50.0, 100.0);
+ Rectangle2D dataArea = new Rectangle2D.Double(10.0, 50.0, 400.0, 300.0);
+ double y1 = axis.java2DToValue(75.0, dataArea, RectangleEdge.LEFT);
+ assertEquals(94.3874312681693, y1, EPSILON);
+ double y2 = axis.java2DToValue(75.0, dataArea, RectangleEdge.RIGHT);
+ assertEquals(94.3874312681693, y2, EPSILON);
+ double x1 = axis.java2DToValue(75.0, dataArea, RectangleEdge.TOP);
+ assertEquals(55.961246381405, x1, EPSILON);
+ double x2 = axis.java2DToValue(75.0, dataArea, RectangleEdge.BOTTOM);
+ assertEquals(55.961246381405, x2, EPSILON);
+ axis.setInverted(true);
+ double y3 = axis.java2DToValue(75.0, dataArea, RectangleEdge.LEFT);
+ assertEquals(52.9731547179647, y3, EPSILON);
+ double y4 = axis.java2DToValue(75.0, dataArea, RectangleEdge.RIGHT);
+ assertEquals(52.9731547179647, y4, EPSILON);
+ double x3 = axis.java2DToValue(75.0, dataArea, RectangleEdge.TOP);
+ assertEquals(89.3475453695651, x3, EPSILON);
+ double x4 = axis.java2DToValue(75.0, dataArea, RectangleEdge.BOTTOM);
+ assertEquals(89.3475453695651, x4, EPSILON);
+ }
+
+
+ /**
+ * A simple test for the auto-range calculation looking at a
+ * LogAxis used as the range axis for a CategoryPlot.
+ */
+ @Test
+ public void testAutoRange1() {
+ DefaultCategoryDataset