package info.limpet.stackedcharts.ui.view; import java.awt.Graphics2D; import java.awt.geom.Rectangle2D; import org.apache.commons.math3.analysis.interpolation.LinearInterpolator; import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRendererState; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.util.LineUtilities; import org.jfree.data.xy.XYDataset; import org.jfree.ui.RectangleEdge; public class WrappingRenderer extends XYLineAndShapeRenderer { /** * This an immutable class for containing two values together. * * @author aris */ public class ImmutablePair<T0, T1> { protected final T0 left; protected final T1 right; public ImmutablePair(T0 left, T1 right) { this.left = left; this.right = right; } /** * @return the left value of the pair */ public T0 getLeft() { return left; } /** * @return the right value of the pair */ public T1 getRight() { return right; } } /** * An interface for creating custom logic for drawing lines between points for * XYLineAndShapeRenderer. */ private static interface OverflowCondition { /** * Custom logic for detecting overflow between points. * * @param y0 * previous y * @param x0 * previous x * @param y1 * current y * @param x1 * current x * @return true, if you there is an overflow detected. Otherwise, return false */ public boolean isOverflow(double y0, double x0, double y1, double x1); } /** * */ private static final long serialVersionUID = 1L; private final double min; private final double max; private final double range; private final LinearInterpolator interpolator = new LinearInterpolator(); private final OverflowCondition overflowCondition; /** * * @param min * the minimum wrapping value * @param max * the maximum wrapping value * @param midOrigin * whether the axis range should have zero at origin */ public WrappingRenderer(final double min, final double max) { this.min = min; this.max = max; this.range = max - min; overflowCondition = new OverflowCondition() { @Override public boolean isOverflow(double y0, double x0, double y1, double x1) { return Math.abs(y1 - y0) > 90d; } }; } @Override protected void drawPrimaryLineAsPath(XYItemRendererState state, Graphics2D g2, XYPlot plot, XYDataset dataset, int pass, int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis, Rectangle2D dataArea) { // get the data point... State s = (State) state; try { double x1 = dataset.getXValue(series, item); double y1 = dataset.getYValue(series, item); if (Double.isNaN(x1) && Double.isNaN(y1)) { s.setLastPointGood(false); return; } // double check values valid y1 = y1 < min ? y1 + range : y1; y1 = y1 > max ? y1 - range : y1; if (!s.isLastPointGood()) { ImmutablePair<Float, Float> xy = translate(plot, domainAxis, rangeAxis, dataArea, x1, y1); s.seriesPath.moveTo(xy.getLeft(), xy.getRight()); s.setLastPointGood(true); return; } // and the previous value double x0 = dataset.getXValue(series, item - 1); double y0 = dataset.getYValue(series, item - 1); // double check values valid y0 = y0 > max ? y0 - range : y0; y0 = y0 < min ? y0 + range : y0; if (overflowCondition.isOverflow(y0, x0, y1, x1)) { boolean overflowAtMax = y1 < y0; if (overflowAtMax) { PolynomialSplineFunction psf = interpolator.interpolate(new double[] {y0, y1 + range}, new double[] {x0, x1}); double xmid = psf.value(max); ImmutablePair<Float, Float> xy = translate(plot, domainAxis, rangeAxis, dataArea, xmid, max); s.seriesPath.lineTo(xy.getLeft(), xy.getRight()); xy = translate(plot, domainAxis, rangeAxis, dataArea, xmid, min); s.seriesPath.moveTo(xy.getLeft(), xy.getRight()); xy = translate(plot, domainAxis, rangeAxis, dataArea, x1, y1); s.seriesPath.lineTo(xy.getLeft(), xy.getRight()); } else { PolynomialSplineFunction psf = interpolator.interpolate(new double[] {y1 - range, y0}, new double[] {x1, x0}); double xmid = psf.value(min); ImmutablePair<Float, Float> xy = translate(plot, domainAxis, rangeAxis, dataArea, xmid, min); s.seriesPath.lineTo(xy.getLeft(), xy.getRight()); xy = translate(plot, domainAxis, rangeAxis, dataArea, xmid, max); s.seriesPath.moveTo(xy.getLeft(), xy.getRight()); xy = translate(plot, domainAxis, rangeAxis, dataArea, x1, y1); s.seriesPath.lineTo(xy.getLeft(), xy.getRight()); } } else { ImmutablePair<Float, Float> xy = translate(plot, domainAxis, rangeAxis, dataArea, x1, y1); s.seriesPath.lineTo(xy.getLeft(), xy.getRight()); } s.setLastPointGood(true); } finally { // if this is the last item, draw the path ... if (item == s.getLastItemIndex()) { // draw path drawFirstPassShape(g2, pass, series, item, s.seriesPath); } } } private ImmutablePair<Float, Float> translate(XYPlot plot, ValueAxis domainAxis, ValueAxis rangeAxis, Rectangle2D dataArea, double x, double y) { RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); double transX1 = domainAxis.valueToJava2D(x, dataArea, xAxisLocation); double transY1 = rangeAxis.valueToJava2D(y, dataArea, yAxisLocation); // update path to reflect latest point float xtrans = (float) transX1; float ytrans = (float) transY1; PlotOrientation orientation = plot.getOrientation(); if (orientation == PlotOrientation.HORIZONTAL) { xtrans = (float) transY1; ytrans = (float) transX1; } return new ImmutablePair<>(xtrans, ytrans); } @Override protected void drawPrimaryLine(XYItemRendererState state, Graphics2D g2, XYPlot plot, XYDataset dataset, int pass, int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis, Rectangle2D dataArea) { if (item == 0) { return; } // get the data point... double x1 = dataset.getXValue(series, item); double y1 = dataset.getYValue(series, item); if (Double.isNaN(y1) || Double.isNaN(x1)) { return; } double x0 = dataset.getXValue(series, item - 1); double y0 = dataset.getYValue(series, item - 1); if (Double.isNaN(y0) || Double.isNaN(x0)) { return; } // double check values valid (not greater than max) y0 = y0 > max ? y0 - range : y0; y1 = y1 > max ? y1 - range : y1; // double check values valid (not less than min) y0 = y0 < min ? y0 + range : y0; y1 = y1 < min ? y1 + range : y1; if (overflowCondition.isOverflow(y0, x0, y1, x1)) { boolean overflowAtMax = y1 < y0; if (overflowAtMax) { PolynomialSplineFunction psf = interpolator.interpolate(new double[] {y0, y1 + (max - min)}, new double[] {x0, x1}); double xmid = psf.value(max); drawPrimaryLine(state, g2, plot, x0, y0, xmid, max, pass, series, item, domainAxis, rangeAxis, dataArea); drawPrimaryLine(state, g2, plot, xmid, min, x1, y1, pass, series, item, domainAxis, rangeAxis, dataArea); } else { PolynomialSplineFunction psf = interpolator.interpolate(new double[] {y1 - (max - min), y0}, new double[] {x1, x0}); double xmid = psf.value(min); drawPrimaryLine(state, g2, plot, x0, y0, xmid, min, pass, series, item, domainAxis, rangeAxis, dataArea); drawPrimaryLine(state, g2, plot, xmid, max, x1, y1, pass, series, item, domainAxis, rangeAxis, dataArea); } } else { drawPrimaryLine(state, g2, plot, x0, y0, x1, y1, pass, series, item, domainAxis, rangeAxis, dataArea); } } private void drawPrimaryLine(XYItemRendererState state, Graphics2D g2, XYPlot plot, double x0, double y0, double x1, double y1, int pass, int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis, Rectangle2D dataArea) { RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); double transX0 = domainAxis.valueToJava2D(x0, dataArea, xAxisLocation); double transY0 = rangeAxis.valueToJava2D(y0, dataArea, yAxisLocation); double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); // only draw if we have good values if (Double.isNaN(transX0) || Double.isNaN(transY0) || Double.isNaN(transX1) || Double.isNaN(transY1)) { return; } PlotOrientation orientation = plot.getOrientation(); boolean visible; if (orientation == PlotOrientation.HORIZONTAL) { state.workingLine.setLine(transY0, transX0, transY1, transX1); } else if (orientation == PlotOrientation.VERTICAL) { state.workingLine.setLine(transX0, transY0, transX1, transY1); } visible = LineUtilities.clipLine(state.workingLine, dataArea); if (visible) { drawFirstPassShape(g2, pass, series, item, state.workingLine); } } }