jfreechart icon indicating copy to clipboard operation
jfreechart copied to clipboard

XYDifferenceRenderer: wrong paint switch with 'null' values

Open b2405 opened this issue 4 years ago • 3 comments

I have TimeSeries's where some TimeSeriesDataItem might have 'null' values. When using XYDifferenceRenderer I observed that the switch between positive and negative paint is out of order. Below are the screenshots from some test cases showing the behavior for three test cases: a) TimeSeries with identical nb of TimeSeriesDataItem and none having 'null' values --> correct switch positive / negative paint b) TimeSeries with missing TimeSeriesDateItem and none having 'null' values --> correct switch positive / negative paint c) TimeSeries with identical nb of TimeSeriesDataItem and some are having 'null' values --> incorrect switch positive / negative paint Case a) correct switch positve / negative paint testTimeSeriesRectWave2020-11-14

Case b) correct switch positive / negative paint testTimeSeriesRectWaveMisSteps2020-11-14

Case c) incorrect switch positive / negative paint testTimeSeriesRectWaveNullValues2020-11-14

b2405 avatar Nov 14 '20 21:11 b2405

As suggested here, the presence of null values makes a TimeSeries discontiuous. I'm not sure how XYDifferenceRenderer could deal with this in general. Does your domain suggest a suitable threshold/sentinel value that might serve as a proxy for missing values?

trashgod avatar Nov 16 '20 00:11 trashgod

Today my import class is not yet specifically taking care of situations with missing values. I was already thinking about a solution to tread missing value at level of my import class (prefered solution: eliminate complete TimeSeriesDataItem with null values). On the other hand I will study the source code for XYDifferenceRenderer to better understand it's behavior.

For XYLineAndShapeRenderer I'm aware of discontinuous TimeSeries behavior having null values. Such my assumption had been a similar situation might be given for XYDifferenceRenderer.

b2405 avatar Nov 16 '20 19:11 b2405

Today, after spending significant amount of time (mainly to write code for automated graph analysis) I finished work on updated XYDiffRenderer.java class which is based on XYDifferenceRenderer.java, but has implemented handling of potential null values. Attached is the result for potential further usage (without any warranty). The major change is in method protected void drawItemPass0

testXYDiffR3withChecks_test_XYDiffRenderer: 9

`/** * */ package main.chart;

/**

  • @author bgs02

*/ public class XYDiffRenderer extends XYDifferenceRenderer {

/** serial UID */
private static final long serialVersionUID = -1866264585870884648L;

/** The paint used to highlight positive differences (y(0) > y(1)). */
private transient Paint positivePaint;

/** The paint used to highlight negative differences (y(0) < y(1)). */
private transient Paint negativePaint;

/** Display shapes at each point? */
private boolean shapesVisible;

/** The shape to display in the legend item. */
private transient Shape legendLine;

/**
 * This flag controls whether or not the x-coordinates (in Java2D space) are
 * rounded to integers. When set to true, this can avoid the vertical striping
 * that anti-aliasing can generate. However, the rounding may not be appropriate
 * for output in high resolution formats (for example, vector graphics formats
 * such as SVG and PDF).
 */
private boolean roundXCoordinates;

/** logger **/
@SuppressWarnings("exports")
public Logger logger = null;

/**
 * Default constructor
 */
public XYDiffRenderer() {
	this(Color.green, Color.RED, false);
}

/**
 * Constructor with option to define colors and shape
 * 
 * @param positivePaint
 * @param negativePaint
 * @param shapes
 */
@SuppressWarnings("exports")
public XYDiffRenderer(Paint positivePaint, Paint negativePaint, boolean shapes) {
	super(positivePaint, negativePaint, shapes);
	logger = Logger.getLogger(XYDiffRenderer.class.getName());
	Args.nullNotPermitted(positivePaint, "positivePaint");
	Args.nullNotPermitted(negativePaint, "negativePaint");
	this.positivePaint = positivePaint;
	this.negativePaint = negativePaint;
	this.shapesVisible = shapes;
	this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
	this.roundXCoordinates = false;
}

/**
 * Returns the paint used to highlight positive differences.
 *
 * @return The paint (never {@code null}).
 * @see #setPositivePaint(Paint)
 */
public Paint getPositivePaint() {
	return this.positivePaint;
}

//........

// Helper method for testing only
private String getDateString(double value) {
	FixedMillisecond fms = new FixedMillisecond((long) value);
	return fms.getStart().toString();
}

/**
 * Draws the visual representation of a single data item, first pass.
 *
 * @param xGraphics       the graphics device.
 * @param xDataArea       the area within which the data is being drawn.
 * @param xPinfo          collects information about the drawing.
 * @param xPlot           the plot (can be used to obtain standard color
 *                        information etc).
 * @param xDomainAxis     the domain (horizontal) axis.
 * @param xRangeAxis      the range (vertical) axis.
 * @param xDataset        the dataset.
 * @param xSeries         the series index (zero-based).
 * @param xItem           the item index (zero-based).
 * @param xCrosshairState crosshair information for the plot ({@code null}
 *                        permitted).
 * @implSpec a) shall check if both series are overlapping <br>
 *           b) shall check if series contains at least 2 data item<br>
 *           c) shall check minuend and subtrahend for valid y values at each
 *           step<br>
 *           d) shall step over current x values if one of current y values is
 *           of {@code null} <br>
 *           e) shall clip leading and ending tails to minuend or subtrahend
 *           <br>
 *           e) in case of next y value is of {@code null} value clip last valid
 *           point to minuend or subtrahend and forward to next point<br>
 *           e) with previous point of value {@code null} clip to minuend or
 *           subtrahend and proceed to next point
 * @implNote loops through data and advances alternately minuend or subtrahend
 */
protected void drawItemPass0(Graphics2D xGraphics, Rectangle2D xDataArea, PlotRenderingInfo xPinfo, XYPlot xPlot,
		ValueAxis xDomainAxis, ValueAxis xRangeAxis, XYDataset xDataset, int xSeries, int xItem,
		CrosshairState xCrosshairState) {

	if (!((0 == xSeries) && (0 == xItem))) {
		// renderer calls in reveres order, such loop through first series till index 0
		// of minuend series is found
		return;
	}

	// in case XYDataset has only a single series, set subtrahend as implied zero
	boolean bImpliedZeroSubtrahend = (1 == xDataset.getSeriesCount());

	// check if either series is a degenerate case (i.e. has less than 2 points)
	if (isEitherSeriesDegenerate(xDataset, bImpliedZeroSubtrahend)) {
		return;
	}

	// check if series are disjoint (i.e. domain-spans do not overlap)
	if (!bImpliedZeroSubtrahend && areSeriesDisjoint(xDataset)) {
		return;
	}

	// LinkedLists (use prefix lL) for polygon creation
	LinkedList<Double> lLMinuendXs = new LinkedList<Double>();
	LinkedList<Double> lLMinuendYs = new LinkedList<Double>();
	LinkedList<Double> lLSubtrahendXs = new LinkedList<Double>();
	LinkedList<Double> lLSubtrahendYs = new LinkedList<Double>();
	LinkedList<Double> lLPolygonXs = new LinkedList<Double>();
	LinkedList<Double> lLPolygonYs = new LinkedList<Double>();

	// state (int: use prefix i, Double: prefix od, double: prefix d)
	int iMinuendItem = 0; // minuend index
	int iMinuendItemCount = xDataset.getItemCount(0);
	Double odMinuendCurX = null; // current minuend X; dX1
	Double odMinuendNextX = null; // next minuend X; dX2
	Double odMinuendCurY = null; // current minuend Y; dY1
	Double odMinuendNextY = null; // current subtrahend Y; dY2
	double dMinuendMaxY = Double.NEGATIVE_INFINITY;
	double dMinuendMinY = Double.POSITIVE_INFINITY;

	int iSubtrahendItem = 0; // subtrahend index
	int iSubtrahendItemCount = 0;
	Double odSubtrahendCurX = null; // current subtrahend X; dX3
	Double odSubtrahendNextX = null; // next subtrahend X; dX4
	Double odSubtrahendCurY = null; // current subtrahend Y; dY3
	Double odSubtrahendNextY = null; // next subtrahend Y; dY4
	double dSubtrahendMaxY = Double.NEGATIVE_INFINITY;
	double dSubtrahendMinY = Double.POSITIVE_INFINITY;

	// if subtrahend is not specified (bImpliedZeroSubtrahend == true), set it zero
	if (bImpliedZeroSubtrahend) {
		iSubtrahendItem = 0;
		iSubtrahendItemCount = 2;
		odSubtrahendCurX = Double.valueOf(xDataset.getXValue(0, 0));
		odSubtrahendNextX = Double.valueOf(xDataset.getXValue(0, (iMinuendItemCount - 1)));
		odSubtrahendCurY = Double.valueOf(0.0);
		odSubtrahendNextY = Double.valueOf(0.0);
		dSubtrahendMaxY = 0.0;
		dSubtrahendMinY = 0.0;

		lLSubtrahendXs.add(odSubtrahendCurX);
		lLSubtrahendYs.add(odSubtrahendCurY);
	} else {
		iSubtrahendItemCount = xDataset.getItemCount(1);
	}

	// flag to identify TimeSeries class
	boolean bTimeSeries = false;
	if (xDataset instanceof TimeSeriesCollection) {
		bTimeSeries = true;
	}
	// flag to handle minuend series (boolean: use prefix b)
	boolean bMinuendDone = false;
	boolean bMinuendAdvanced = true;
	boolean bMinuendAtIntersect = false;
	// flag to fast forward to next minuend x/y data, used to handle leading tails
	boolean bMinuendFastForward = false;
	// flag to handle null value on minuend series
	boolean bMinuendNullY1 = false;
	boolean bMinuendNullY2 = false;
	boolean bMinuendNullY1Previous = false;
	// flag to handle subtrahend series
	boolean bSubtrahendDone = false;
	boolean bSubtrahendAdvanced = true;
	boolean bSubtrahendAtIntersect = false;
	// flag to fast forward to next subtrahend data, used to handle leading tails
	boolean bSubtrahendFastForward = false;
	// flag to handle null value on subtrahend series
	boolean bSubtrahendNullY3 = false;
	boolean bSubtrahendNullY4 = false;
	boolean bSubtrahendNullY3Previous = false;
	// flag indicating colinear status
	boolean bColinear = false;
	// flag indicating positive result (minuend - subtrahend)
	boolean bPositive;

	// coordinate pairs
	double dX1 = 0.0, dY1 = 0.0; // current minuend point
	double dX2 = 0.0, dY2 = 0.0; // next minuend point
	double dX3 = 0.0, dY3 = 0.0; // current subtrahend point
	double dX4 = 0.0, dY4 = 0.0; // next subtrahend point

	// fast-forward through leading tails
	boolean bFastForwardDone = false;
	while (!bFastForwardDone) {
		logger.log(Level.FINEST, "fast forward leading tails");
		// get x/y data from series at index 0 == minuend series
		dX1 = xDataset.getXValue(0, iMinuendItem);
		dY1 = xDataset.getYValue(0, iMinuendItem);
		dX2 = xDataset.getXValue(0, iMinuendItem + 1);
		dY2 = xDataset.getYValue(0, iMinuendItem + 1);
		if (logger.isLoggable(Level.FINEST)) {
			String str1 = "", str2 = "";
			if (bTimeSeries) {
				str1 = getDateString(dX1);
				str2 = getDateString(dX2);
			} else {
				str1 = String.valueOf(dX1);
				str2 = String.valueOf(dX2);
			}
			String msg = "current iMinuendItem = " + iMinuendItem + "; dX1/dY1 = " + str1 + "/" + dY1
					+ "; dX2/dY2 = " + str2 + "/" + dY2;
			logger.log(Level.FINEST, msg);
		}
		odMinuendCurX = Double.valueOf(dX1);
		odMinuendCurY = Double.valueOf(dY1);
		odMinuendNextX = Double.valueOf(dX2);
		odMinuendNextY = Double.valueOf(dY2);

		// set flags for null values
		bMinuendNullY1 = Double.isNaN(dY1) ? true : false;
		bMinuendNullY2 = Double.isNaN(dY2) ? true : false;

		if (bImpliedZeroSubtrahend) {
			// case single series only
			dX3 = odSubtrahendCurX.doubleValue();
			dY3 = odSubtrahendCurY.doubleValue();
			dX4 = odSubtrahendNextX.doubleValue();
			dY4 = odSubtrahendNextY.doubleValue();
		} else {
			// get x/y data from series at index 1 == subtrahend series
			dX3 = xDataset.getXValue(1, iSubtrahendItem);
			dY3 = xDataset.getYValue(1, iSubtrahendItem);
			dX4 = xDataset.getXValue(1, iSubtrahendItem + 1);
			dY4 = xDataset.getYValue(1, iSubtrahendItem + 1);
			if (logger.isLoggable(Level.FINEST)) {
				String str1 = "", str2 = "";
				if (bTimeSeries) {
					str1 = getDateString(dX3);
					str2 = getDateString(dX4);
				} else {
					str1 = String.valueOf(dX3);
					str2 = String.valueOf(dX4);
				}
				String msg = "current iMinuendItem = " + iMinuendItem + "; dX1/dY1 = " + str1 + "/" + dY3
						+ "; dX2/dY2 = " + str2 + "/" + dY4;
				logger.log(Level.FINEST, msg);
			}
			odSubtrahendCurX = Double.valueOf(dX3);
			odSubtrahendCurY = Double.valueOf(dY3);
			odSubtrahendNextX = Double.valueOf(dX4);
			odSubtrahendNextY = Double.valueOf(dY4);

			// set flags for null value
			bSubtrahendNullY3 = Double.isNaN(dY3) ? true : false;
			bSubtrahendNullY4 = Double.isNaN(dY4) ? true : false;

		}

		// handle 'null' values in leading tail
		if (bMinuendNullY1 | bMinuendNullY2 | bSubtrahendNullY3 | bSubtrahendNullY4) {
			// fast forward both minuend and subtrahend
			if (logger.isLoggable(Level.FINEST)) {
				String msg = "bMinuendNullY1 | bMinuendNullY2 | bSubtrahendNullY3 | bSubtrahendNullY4: "
						+ bMinuendNullY1 + "|" + bMinuendNullY2 + "|" + bSubtrahendNullY3 + "|" + bSubtrahendNullY4;
				logger.log(Level.FINEST, msg);
			}
			iMinuendItem++;
			iSubtrahendItem++;
			bMinuendDone = (iMinuendItem == (iMinuendItemCount - 1));
			bSubtrahendDone = (iSubtrahendItem == (iSubtrahendItemCount - 1));
			bFastForwardDone = ((bMinuendDone | bSubtrahendDone) ? true : false);
			// set flags for previous null value
			bMinuendNullY1Previous = bMinuendNullY1 ? true : false;
			bSubtrahendNullY3Previous = bSubtrahendNullY3 ? true : false;
			// reset current flags
			bMinuendNullY1 = false;
			bMinuendNullY2 = false;
			bSubtrahendNullY3 = false;
			bSubtrahendNullY4 = false;
			continue;
		}

		// fast forward for line segment overlapping
		if (dX2 < dX3) {
			iMinuendItem++;
			bMinuendFastForward = true;
			bMinuendNullY1Previous = bMinuendNullY1 ? true : false;
			continue;
		}
		if (dX4 < dX1) {
			iSubtrahendItem++;
			bSubtrahendFastForward = true;
			bSubtrahendNullY3Previous = bSubtrahendNullY3 ? true : false;
			continue;
		}

		// check if initial polygon needs to be clipped
		boolean bLTailClipped = true;
		if (bLTailClipped & (dX3 <= dX1) && (dX1 <= dX4)) {
			bLTailClipped = false;
			double dSlope = (dY4 - dY3) / (dX4 - dX3);
			odSubtrahendCurX = odMinuendCurX;
			odSubtrahendCurY = Double.valueOf(dSlope * (dX1 - dX3) + dY3);
			lLSubtrahendXs.add(odSubtrahendCurX);
			lLSubtrahendYs.add(odSubtrahendCurY);
		}
		if (bLTailClipped & (dX1 <= dX3) && (dX3 <= dX2)) {
			double dSlope = (dY2 - dY1) / (dX2 - dX1);
			odMinuendCurX = odSubtrahendCurX;
			odMinuendCurY = Double.valueOf(dSlope * (dX3 - dX1) + dY1);
			lLMinuendXs.add(odMinuendCurX);
			lLMinuendYs.add(odMinuendCurY);
		}

		bFastForwardDone = true;
		if (logger.isLoggable(Level.FINEST)) {
			String msg = "leading tail: bMinuendNullY1Previous / bMinuendNullY1 / bMinuendNullY2 = "
					+ bMinuendNullY1Previous + "/" + bMinuendNullY1 + "/" + bMinuendNullY2
					+ "/nbSubtrahendNullY3Previous / bSubtrahendNullY3 / bSubtrahendNullY4"
					+ bSubtrahendNullY3Previous + "/" + bSubtrahendNullY3 + "/" + bSubtrahendNullY4;
			logger.log(Level.FINEST, msg);
		}
	}

	// start of algorithm including null value handling
	while (!bMinuendDone && !bSubtrahendDone) {
		if (logger.isLoggable(Level.FINEST)) {
			String msg = "core algo ca:\nb*Done = " + bMinuendDone + "/" + bSubtrahendDone + ";\nb*FastForward = "
					+ bMinuendFastForward + "/" + bSubtrahendFastForward + ";\nb*Advanced = " + bMinuendAdvanced
					+ "/" + bSubtrahendAdvanced + ";\ni*Item = " + iMinuendItem + "/" + iSubtrahendItem;
			logger.log(Level.FINEST, msg);
		}

		if (!bMinuendDone && !bMinuendFastForward && bMinuendAdvanced) {
			dX1 = xDataset.getXValue(0, iMinuendItem);
			dY1 = xDataset.getYValue(0, iMinuendItem);
			odMinuendCurX = Double.valueOf(dX1);
			odMinuendCurY = Double.valueOf(dY1);
			bMinuendNullY1 = Double.isNaN(dY1) ? true : false;

			if (!bMinuendAtIntersect & !bMinuendNullY1) {
				lLMinuendXs.add(odMinuendCurX);
				lLMinuendYs.add(odMinuendCurY);
			}

			dX2 = xDataset.getXValue(0, iMinuendItem + 1);
			dY2 = xDataset.getYValue(0, iMinuendItem + 1);
			odMinuendNextX = Double.valueOf(dX2);
			odMinuendNextY = Double.valueOf(dY2);
			bMinuendNullY2 = Double.isNaN(dY2) ? true : false;

			if (logger.isLoggable(Level.FINEST)) {
				String str1 = "", str2 = "";
				if (bTimeSeries) {
					str1 = getDateString(dX1);
					str2 = getDateString(dX2);
				} else {
					str1 = String.valueOf(dX1);
					str2 = String.valueOf(dX2);
				}
				String msg = "current iMinuendItem = " + iMinuendItem + "; dX1/dY1 = " + str1 + "/" + dY1
						+ "; dX2/dY2 = " + str2 + "/" + dY2;
				logger.log(Level.FINEST, msg);
			}
		}

		// never updated the subtrahend if it is implied to be zero
		if (!bImpliedZeroSubtrahend && !bSubtrahendDone && !bSubtrahendFastForward && bSubtrahendAdvanced) {
			dX3 = xDataset.getXValue(1, iSubtrahendItem);
			dY3 = xDataset.getYValue(1, iSubtrahendItem);
			odSubtrahendCurX = Double.valueOf(dX3);
			odSubtrahendCurY = Double.valueOf(dY3);
			bSubtrahendNullY3 = Double.isNaN(dY3) ? true : false;

			if (!bSubtrahendAtIntersect & !bSubtrahendNullY3) {
				lLSubtrahendXs.add(odSubtrahendCurX);
				lLSubtrahendYs.add(odSubtrahendCurY);
			}

			dX4 = xDataset.getXValue(1, iSubtrahendItem + 1);
			dY4 = xDataset.getYValue(1, iSubtrahendItem + 1);
			odSubtrahendNextX = Double.valueOf(dX4);
			odSubtrahendNextY = Double.valueOf(dY4);
			bSubtrahendNullY4 = Double.isNaN(dY4) ? true : false;
			if (logger.isLoggable(Level.FINEST)) {
				String str1 = "", str2 = "";
				if (bTimeSeries) {
					str1 = getDateString(dX3);
					str2 = getDateString(dX4);
				} else {
					str1 = String.valueOf(dX3);
					str2 = String.valueOf(dX4);
				}
				String msg = "current iMinuendItem = " + iMinuendItem + "; dX3/dY3 = " + str1 + "/" + dY3
						+ "; dX4/dY4 = " + str2 + "/" + dY4;
				logger.log(Level.FINEST, msg);
			}
		}

		/**
		 * use case 1: 'null' values at (x1/y1) or (x3/y3) but not at (x2/y2) and not at
		 * (x4/y4)<br>
		 * advance if null value AND advance if non-overlapping situation: if subtrahend
		 * is in advance: x3 >= x2 --> forward minuend; if minuend is in advance: x1 >=
		 * x4 --> forward subtrahend;
		 **/
		if ((bMinuendNullY1 | bSubtrahendNullY3) & !bMinuendNullY2 & !bSubtrahendNullY4) {
			if (logger.isLoggable(Level.FINEST)) {
				String msg = "use case 1 \nbMinuendNullY1 / bSubtrahendNullY3 / bMinuendNullY2 / bSubtrahendNullY4 = "
						+ bMinuendNullY1 + "/" + bSubtrahendNullY3 + "/" + bMinuendNullY2 + "/" + bSubtrahendNullY4;
				logger.log(Level.FINEST, msg);
			}

			// clear LinkedList's for restart after 'null' value
			lLMinuendXs.clear();
			lLMinuendYs.clear();
			lLSubtrahendXs.clear();
			lLSubtrahendYs.clear();
			lLPolygonXs.clear();
			lLPolygonYs.clear();

			// reset flags 'null' value for current and set flag for previous
			bMinuendNullY1Previous = bMinuendNullY1;
			bSubtrahendNullY3Previous = bSubtrahendNullY3;
			if (logger.isLoggable(Level.FINEST)) {
				String msg = "use case 1 \nbMinuendNullY1Previous / bSubtrahendNullY3Previous = "
						+ bMinuendNullY1Previous + "/" + bSubtrahendNullY3Previous;
				logger.log(Level.FINEST, msg);
			}

			// advance minuend / subtrahend depending on null value
			if (bMinuendNullY1) {
				iMinuendItem++;// step to next item
				bMinuendAdvanced = true;
				bMinuendNullY1 = false;// reset flag
			} else {
				bMinuendAdvanced = false;
			}
			if (bSubtrahendNullY3) {
				iSubtrahendItem++;// step to next item
				bSubtrahendAdvanced = true;
				bSubtrahendNullY3 = false;// reset flag
			} else {
				bSubtrahendAdvanced = false;
			}

			// check potential non-overlapping on future status; future x3 is current x4
			if (!bMinuendAdvanced & dX4 >= dX2) {
				iMinuendItem++;
				bMinuendAdvanced = true;
				bMinuendNullY1 = false;
			}
			// check potential non-overlapping on future status; future x1 is current x2
			if (!bSubtrahendAdvanced & dX2 >= dX4) {
				iSubtrahendItem++;
				bSubtrahendAdvanced = true;
				bSubtrahendNullY3 = false;
			}
			// test for end of data
			bMinuendDone = (iMinuendItem == (iMinuendItemCount - 1));
			bSubtrahendDone = (iSubtrahendItem == (iSubtrahendItemCount - 1));
			continue;
		}

		/**
		 * use case 2: 'null' values at (x2/y2) or (x4/y4) but not at (x1/y1), not at
		 * (x3/y3), not at previous(x1/y1) and not at previous(x3/y3)<br>
		 * finalize polygon
		 **/
		if ((bMinuendNullY2 | bSubtrahendNullY4) & !bMinuendNullY1 & !bSubtrahendNullY3 & !bMinuendNullY1Previous
				& !bSubtrahendNullY3Previous) {
			if (logger.isLoggable(Level.FINEST)) {
				String msg = "use case 2 \nbMinuendNullY2 / bSubtrahendNullY4 / bMinuendNullY1 / bSubtrahendNullY3 / bMinuendNullY1Previous / bSubtrahendNullY3Previous"
						+ bMinuendNullY2 + "/" + bSubtrahendNullY4 + "/" + bMinuendNullY1 + "/" + bSubtrahendNullY3
						+ "/" + bMinuendNullY1Previous + "/" + bSubtrahendNullY3Previous;
				logger.log(Level.FINEST, msg);
			}

			// check if polygon needs to be clipped to subtrahend or minuend
			if ((bMinuendNullY2 & !bSubtrahendNullY4) && (dX3 <= dX1) && (dX1 <= dX4)) {
				double dSlope = (dY4 - dY3) / (dX4 - dX3);
				odSubtrahendCurX = Double.valueOf(dX1);
				odSubtrahendCurY = Double.valueOf(dSlope * (dX1 - dX3) + dY3);
				lLSubtrahendXs.add(odSubtrahendCurX);
				lLSubtrahendYs.add(odSubtrahendCurY);
			}
			if ((bSubtrahendNullY4 & !bMinuendNullY1) && (dX1 <= dX3) && (dX3 <= dX2)) {
				double dSlope = (dY2 - dY1) / (dX2 - dX1);
				odMinuendCurX = Double.valueOf(dX3);
				odMinuendCurY = Double.valueOf(dSlope * (dX3 - dX1) + dY1);
				lLMinuendXs.add(odMinuendCurX);
				lLMinuendYs.add(odMinuendCurY);
			}

			try {
				// reverse subtrahend points before adding to polygon
				Collections.reverse(lLSubtrahendXs);
				Collections.reverse(lLSubtrahendYs);
				lLPolygonXs.addAll(lLMinuendXs);
				lLPolygonXs.addAll(lLSubtrahendXs);
				lLPolygonYs.addAll(lLMinuendYs);
				lLPolygonYs.addAll(lLSubtrahendYs);

				dSubtrahendMaxY = Collections.max(lLSubtrahendYs);
				dMinuendMaxY = Collections.max(lLMinuendYs);
				dSubtrahendMinY = Collections.min(lLSubtrahendYs);
				dMinuendMinY = Collections.min(lLMinuendYs);
				bPositive = (dSubtrahendMaxY <= dMinuendMaxY) && (dSubtrahendMinY <= dMinuendMinY);

				if (logger.isLoggable(Level.FINEST)) {
					StringBuilder sb = new StringBuilder();
					sb.append("use case 2\n");
					sb.append("bPositive = " + bPositive + "\n");
					if (bTimeSeries) {
						lLPolygonXs.forEach(e -> sb.append("polygonXs = " + getDateString(e.doubleValue()) + "\n"));
					} else {
						lLPolygonXs.forEach(e -> sb.append("polygonXs = " + e.doubleValue() + "\n"));
					}
					lLPolygonYs.forEach(e -> sb.append("polygonYs = " + e.doubleValue() + "\n"));
					logger.log(Level.FINEST, sb.toString());
				}

				// create actual polygon
				createPolygon(xGraphics, xDataArea, xPlot, xDomainAxis, xRangeAxis, bPositive, lLPolygonXs,
						lLPolygonYs);
			} catch (NullPointerException | NoSuchElementException e) {
				logger.log(Level.SEVERE, e.getMessage(), e);
			}
			// clear LinkedList's
			lLMinuendXs.clear();
			lLMinuendYs.clear();
			lLSubtrahendXs.clear();
			lLSubtrahendYs.clear();
			lLPolygonXs.clear();
			lLPolygonYs.clear();
			// reset flags for null value
			bMinuendNullY2 = false;
			bSubtrahendNullY4 = false;
			// advance minuend and subtrahend
			iMinuendItem++;
			bMinuendAdvanced = true;
			iSubtrahendItem++;
			bSubtrahendAdvanced = true;
			// test for end of data
			bMinuendDone = (iMinuendItem == (iMinuendItemCount - 1));
			bSubtrahendDone = (iSubtrahendItem == (iSubtrahendItemCount - 1));
			continue;

		}

		/**
		 * use case 3: 'null' values at previous(x1/y1) or previous(x3/y3) but not at
		 * (x1/y1), not at (x3/y3), not at (x2/y2) and not at (x4/y4)<br>
		 * 
		 * start with new polygon
		 **/
		if ((bMinuendNullY1Previous | bSubtrahendNullY3Previous) & !bMinuendNullY1 & !bSubtrahendNullY3
				& !bMinuendNullY2 & !bSubtrahendNullY4) {
			if (logger.isLoggable(Level.FINEST)) {
				String msg = "use case 3 \nbMinuendNullY1Previous / bSubtrahendNullY3Previous / bMinuendNullY1 / bSubtrahendNullY3 / bMinuendNullY2 / bSubtrahendNullY4"
						+ bMinuendNullY1Previous + "/" + bSubtrahendNullY3Previous + "/" + bMinuendNullY1 + "/" + bSubtrahendNullY3
						+ "/" + bMinuendNullY2 + "/" + bSubtrahendNullY4;
				logger.log(Level.FINEST, msg);
			}
			// clear LinkedList's and restart after previous 'null' value
			lLMinuendXs.clear();
			lLMinuendYs.clear();
			lLSubtrahendXs.clear();
			lLSubtrahendYs.clear();
			lLPolygonXs.clear();
			lLPolygonYs.clear();

			// clip to subtrahend or minuend
			if (bMinuendNullY1Previous & (dX3 <= dX1) && (dX1 <= dX4)) {
				// add current minuend (x1/y1) and clip to subtrahend
				lLMinuendXs.add(odMinuendCurX);
				lLMinuendYs.add(odMinuendCurY);
				double dSlope = (dY4 - dY3) / (dX4 - dX3);
				odSubtrahendCurX = Double.valueOf(dX1);
				odSubtrahendCurY = Double.valueOf(dSlope * (dX1 - dX3) + dY3);
				lLSubtrahendXs.add(odSubtrahendCurX);
				lLSubtrahendYs.add(odSubtrahendCurY);
			}
			if (bSubtrahendNullY3Previous & (dX1 <= dX3) && (dX3 <= dX2)) {
				// add current subtrahend (x3/y3) and clip to minuend
				lLSubtrahendXs.add(odSubtrahendCurX);
				lLSubtrahendYs.add(odSubtrahendCurY);
				double dSlope = (dY2 - dY1) / (dX2 - dX1);
				odMinuendCurX = Double.valueOf(dX3);
				odMinuendCurY = Double.valueOf(dSlope * (dX3 - dX1) + dY1);
				lLMinuendXs.add(odMinuendCurX);
				lLMinuendYs.add(odMinuendCurY);
			}

			// reset previous null-value flag such next loop goes to intersection check still using actual values
			bMinuendNullY1Previous = false;
			bSubtrahendNullY3Previous = false;
			
			// test for end of data
			bMinuendDone = (iMinuendItem == (iMinuendItemCount - 1));
			bSubtrahendDone = (iSubtrahendItem == (iSubtrahendItemCount - 1));
			continue;
		}

		/**
		 * use case 4: 'null' value at (x1/y1 or x3/y3) and 'null' value
		 * at (x2/y2 or x4/y4)
		 **/ 
		if ((bMinuendNullY1 | bSubtrahendNullY3) & (bMinuendNullY2 | bSubtrahendNullY4)) {
			if (logger.isLoggable(Level.FINEST)) {
				String msg = "use case 4 \nbMinuendNullY1 / bSubtrahendNullY3 / bMinuendNullY2 / bSubtrahendNullY4 = "
						+ bMinuendNullY1 +"/"+ bSubtrahendNullY3 +"/"+ bMinuendNullY2 +"/"+ bSubtrahendNullY4;
				logger.log(Level.FINEST, msg);
			}

			// clear LinkedList's
			lLMinuendXs.clear();
			lLMinuendYs.clear();
			lLSubtrahendXs.clear();
			lLSubtrahendYs.clear();
			lLPolygonXs.clear();
			lLPolygonYs.clear();
			// set previous null-value flag
			bMinuendNullY1Previous = (bMinuendNullY1 ? true : false);
			bSubtrahendNullY3Previous = (bSubtrahendNullY3 ? true : false);
			// reset flags
			bMinuendNullY1 = false;
			bSubtrahendNullY3 = false;
			bMinuendNullY2 = false;
			bSubtrahendNullY4 = false;
			// advance items
			iMinuendItem++;
			bMinuendAdvanced = true;
			iSubtrahendItem++;
			bSubtrahendAdvanced = true;
			// test for end of data
			bMinuendDone = (iMinuendItem == (iMinuendItemCount - 1));
			bSubtrahendDone = (iSubtrahendItem == (iSubtrahendItemCount - 1));
			continue;
		}
		
		/**
		 * use case 5: no 'null' values
		 */
		logger.log(Level.FINEST, "use case 5");
		// b*FastForward (only matters for 1st time through loop)
		bMinuendFastForward = false;
		bSubtrahendFastForward = false;

		Double odIntersectX = null;
		Double odIntersectY = null;
		boolean bIntersect = false;

		bMinuendAtIntersect = false;
		bSubtrahendAtIntersect = false;

		// check for intersect
		if ((dX2 == dX4) && (dY2 == dY4)) {
			// check if line segments are colinear
			if ((dX1 == dX3) && (dY1 == dY3)) {
				bColinear = true;
			} else {
				// intersect is at next point for both the minuend
				// and subtrahend
				odIntersectX = Double.valueOf(dX2);
				odIntersectY = Double.valueOf(dY2);
				bIntersect = true;
				bMinuendAtIntersect = true;
				bSubtrahendAtIntersect = true;
			}
		} else {
			// compute intersect
			double dDenominator = ((dY4 - dY3) * (dX2 - dX1)) - ((dX4 - dX3) * (dY2 - dY1));
			double dDeltaY = dY1 - dY3;
			double dDeltaX = dX1 - dX3;
			double dNumeratorA = ((dX4 - dX3) * dDeltaY) - ((dY4 - dY3) * dDeltaX);
			double dNumeratorB = ((dX2 - dX1) * dDeltaY) - ((dY2 - dY1) * dDeltaX);
			// check if line segments are colinear
			if ((0 == dNumeratorA) && (0 == dNumeratorB) && (0 == dDenominator)) {
				bColinear = true;
			} else {
				// check if previously colinear
				if (bColinear) {
					// clear colinear points and flag
					lLMinuendXs.clear();
					lLMinuendYs.clear();
					lLSubtrahendXs.clear();
					lLSubtrahendYs.clear();
					lLPolygonXs.clear();
					lLPolygonYs.clear();

					bColinear = false;
					// starting point for new polygon
					boolean bUseMinuend = ((dX3 <= dX1) && (dX1 <= dX4));
					lLPolygonXs.add(bUseMinuend ? odMinuendCurX : odSubtrahendCurX);
					lLPolygonYs.add(bUseMinuend ? odMinuendCurY : odSubtrahendCurY);
				}
			}

			// compute slope components
			double dSlopeA = dNumeratorA / dDenominator;
			double dSlopeB = dNumeratorB / dDenominator;

			// test if both graph have a vertical rise at identical x-value
			boolean bVertical = (dX1 == dX2) && (dX3 == dX4) && (dX2 == dX4);

			// check if line segments intersect
			if (((0 < dSlopeA) && (dSlopeA <= 1) && (0 < dSlopeB) && (dSlopeB <= 1)) || bVertical) {
				// compute point of intersection
				double dXi;
				double dYi;
				if (bVertical) {
					bColinear = false;
					dXi = dX2;
					dYi = dX4;
				} else {
					dXi = dX1 + (dSlopeA * (dX2 - dX1));
					dYi = dY1 + (dSlopeA * (dY2 - dY1));
				}
				
				odIntersectX = Double.valueOf(dXi);
				odIntersectY = Double.valueOf(dYi);
				bIntersect = true;
				bMinuendAtIntersect = ((dXi == dX2) && (dYi == dY2));
				bSubtrahendAtIntersect = ((dXi == dX4) && (dYi == dY4));

				// set minuend and subtrahend to intersect point
				odMinuendCurX = odIntersectX;
				odMinuendCurY = odIntersectY;
				odSubtrahendCurX = odIntersectX;
				odSubtrahendCurY = odIntersectY;
			}
		}

		if (bIntersect) {
			// add the minuend's points to polygon
			lLPolygonXs.addAll(lLMinuendXs);
			lLPolygonYs.addAll(lLMinuendYs);

			// add intersection point to the polygon
			lLPolygonXs.add(odIntersectX);
			lLPolygonYs.add(odIntersectY);

			try {
				// add subtrahend points to polygon in reverse order
				Collections.reverse(lLSubtrahendXs);
				Collections.reverse(lLSubtrahendYs);
				lLPolygonXs.addAll(lLSubtrahendXs);
				lLPolygonYs.addAll(lLSubtrahendYs);

				// compute color setting and create polygon
				dSubtrahendMaxY = (lLSubtrahendYs.isEmpty() ? 0.0 : Collections.max(lLSubtrahendYs));
				dMinuendMaxY = (lLMinuendYs.isEmpty() ? 0.0 : Collections.max(lLMinuendYs));
				dSubtrahendMinY = (lLSubtrahendYs.isEmpty() ? 0.0 : Collections.min(lLSubtrahendYs));
				dMinuendMinY = (lLMinuendYs.isEmpty() ? 0.0 : Collections.min(lLMinuendYs));
				bPositive = (dSubtrahendMaxY <= dMinuendMaxY) && (dSubtrahendMinY <= dMinuendMinY);
				
				if (logger.isLoggable(Level.FINEST)) {
					StringBuilder sb = new StringBuilder();
					sb.append("use case 5\n");
					sb.append("bPositive = " + bPositive + "\n");
					if (bTimeSeries) {
						lLPolygonXs.forEach(e -> sb.append("polygonXs = " + getDateString(e.doubleValue()) + "\n"));
					} else {
						lLPolygonXs.forEach(e -> sb.append("polygonXs = " + e.doubleValue() + "\n"));
					}
					lLPolygonYs.forEach(e -> sb.append("polygonYs = " + e.doubleValue() + "\n"));
					logger.log(Level.FINEST, sb.toString());
				}
				
				createPolygon(xGraphics, xDataArea, xPlot, xDomainAxis, xRangeAxis, bPositive, lLPolygonXs,
						lLPolygonYs);
			} catch (NullPointerException | NoSuchElementException e) {
				logger.log(Level.SEVERE, e.getMessage(), e);
			}

			// clear LinkedList's
			lLMinuendXs.clear();
			lLMinuendYs.clear();
			lLSubtrahendXs.clear();
			lLSubtrahendYs.clear();
			lLPolygonXs.clear();
			lLPolygonYs.clear();

			// start creating new polygon, add intersection point
			lLPolygonXs.add(odIntersectX);
			lLPolygonYs.add(odIntersectY);
		}
		
		// advance the minuend or subtrahend
		if (dX2 <= dX4) {
			iMinuendItem++;
			bMinuendAdvanced = true;
		} else {
			bMinuendAdvanced = false;
		}
		if (!bMinuendAdvanced & dX4 <= dX2) {
			iSubtrahendItem++;
			bSubtrahendAdvanced = true;
		} else {
			bSubtrahendAdvanced = false;
		}
		// test for end of data
		bMinuendDone = (iMinuendItem == (iMinuendItemCount - 1));
		bSubtrahendDone = (iSubtrahendItem == (iSubtrahendItemCount - 1));
	}

	/**
	 * use case 6: ending tail with non-null values
	 **/
	boolean bValuesNotNull = (!bMinuendNullY1 & !bMinuendNullY2 & !bSubtrahendNullY3 & !bSubtrahendNullY4);
	if ((bMinuendDone | bSubtrahendDone) & bValuesNotNull) {
		logger.log(Level.FINEST, "use case 6, ending tail with non-null values");
		if (bMinuendDone & (dX3 <= dX2) & (dX2 <= dX4)) {
			// clip to subtrahend
			double dSlope = (dY4 - dY3) / (dX4 - dX3);
			odSubtrahendNextX = odMinuendNextX;
			odSubtrahendNextY = Double.valueOf((dSlope * dX2) + (dY3 - (dSlope * dX3)));
		}
		if (bSubtrahendDone & (dX1 <= dX4) & (dX4 <= dX2)) {
			// clip to minuend
			double dSlope = (dY2 - dY1) / (dX2 - dX1);
			odMinuendNextX = odSubtrahendNextX;
			odMinuendNextY = Double.valueOf((dSlope * dX4) + (dY1 - (dSlope * dX1)));
		}
	}

	/**
	 * use case 7: ending tail with null values<br>
	 * a) previous x1 or previous x3 had null-values<br>
	 * b) x2 or x4 has null-values
	 **/
	// TODO: still consider potential intersection of current minuend and subtrahend
	// values
	if ((bMinuendDone | bSubtrahendDone) & !bValuesNotNull) {
		if (bMinuendNullY1Previous | bSubtrahendNullY3Previous) {
			lLMinuendXs.clear();
			lLMinuendYs.clear();
			lLSubtrahendXs.clear();
			lLSubtrahendYs.clear();
		}
		if (bMinuendNullY2) {
			lLMinuendXs.add(odMinuendCurX);
			lLMinuendYs.add(odMinuendCurY);
			lLSubtrahendXs.add(odMinuendCurX);
			lLSubtrahendYs.add(odSubtrahendCurY);
		}
		if (bSubtrahendNullY4) {
			lLMinuendXs.add(odSubtrahendCurX);
			lLMinuendYs.add(odMinuendCurY);
			lLSubtrahendXs.add(odSubtrahendCurX);
			lLSubtrahendYs.add(odSubtrahendCurY);
		}
	}

	/**
	 * standard use case: finalize value collection for polygon
	 **/
	if ((!bMinuendDone & !bSubtrahendDone) | bValuesNotNull) {
		lLMinuendXs.add(odMinuendNextX);
		lLMinuendYs.add(odMinuendNextY);
		lLSubtrahendXs.add(odSubtrahendNextX);
		lLSubtrahendYs.add(odSubtrahendNextY);
	}

	try {
		// add the minuend's points
		lLPolygonXs.addAll(lLMinuendXs);
		lLPolygonYs.addAll(lLMinuendYs);
		// add the subtrahend points in reverse order
		Collections.reverse(lLSubtrahendXs);
		Collections.reverse(lLSubtrahendYs);
		lLPolygonXs.addAll(lLSubtrahendXs);
		lLPolygonYs.addAll(lLSubtrahendYs);

		// compute color setting and create polygon
		dSubtrahendMaxY = Collections.max(lLSubtrahendYs);
		dMinuendMaxY = Collections.max(lLMinuendYs);
		dSubtrahendMinY = Collections.min(lLSubtrahendYs);
		dMinuendMinY = Collections.min(lLMinuendYs);
		bPositive = (dSubtrahendMaxY <= dMinuendMaxY) && (dSubtrahendMinY <= dMinuendMinY);
		
		
		if (logger.isLoggable(Level.FINEST)) {
			StringBuilder sb = new StringBuilder();
			sb.append("standard case \n");
			sb.append("bPositive = " + bPositive + "\n");
			if (bTimeSeries) {
				lLPolygonXs.forEach(e -> sb.append("polygonXs = " + getDateString(e.doubleValue()) + "\n"));
			} else {
				lLPolygonXs.forEach(e -> sb.append("polygonXs = " + e.doubleValue() + "\n"));
			}
			lLPolygonYs.forEach(e -> sb.append("polygonYs = " + e.doubleValue() + "\n"));
			logger.log(Level.FINEST, sb.toString());
		}
		
		createPolygon(xGraphics, xDataArea, xPlot, xDomainAxis, xRangeAxis, bPositive, lLPolygonXs, lLPolygonYs);
	} catch (NullPointerException | NoSuchElementException e) {
		logger.log(Level.SEVERE, e.getMessage(), e);
	}

}

/**
 * Draws the visual representation of a single data item, second pass. In the
 * second pass, the renderer draws the lines and shapes for the individual
 * points in the two series.
 *
 * @param x_graphics       the graphics device.
 * @param x_dataArea       the area within which the data is being drawn.
 * @param x_info           collects information about the drawing.
 * @param x_plot           the plot (can be used to obtain standard color
 *                         information etc).
 * @param x_domainAxis     the domain (horizontal) axis.
 * @param x_rangeAxis      the range (vertical) axis.
 * @param x_dataset        the dataset.
 * @param x_series         the series index (zero-based).
 * @param x_item           the item index (zero-based).
 * @param x_crosshairState crosshair information for the plot ({@code null}
 *                         permitted).
 */
protected void drawItemPass1(Graphics2D x_graphics, Rectangle2D x_dataArea, PlotRenderingInfo x_info, XYPlot x_plot,
		ValueAxis x_domainAxis, ValueAxis x_rangeAxis, XYDataset x_dataset, int x_series, int x_item,
		CrosshairState x_crosshairState) {

	Shape l_entityArea = null;
	EntityCollection l_entities = null;
	if (null != x_info) {
		l_entities = x_info.getOwner().getEntityCollection();
	}

	Paint l_seriesPaint = getItemPaint(x_series, x_item);
	Stroke l_seriesStroke = getItemStroke(x_series, x_item);
	x_graphics.setPaint(l_seriesPaint);
	x_graphics.setStroke(l_seriesStroke);

	PlotOrientation l_orientation = x_plot.getOrientation();
	RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge();
	RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge();

	double l_x0 = x_dataset.getXValue(x_series, x_item);
	double l_y0 = x_dataset.getYValue(x_series, x_item);
	double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea, l_domainAxisLocation);
	double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea, l_rangeAxisLocation);

	if (getShapesVisible()) {
		Shape l_shape = getItemShape(x_series, x_item);
		if (l_orientation == PlotOrientation.HORIZONTAL) {
			l_shape = ShapeUtils.createTranslatedShape(l_shape, l_y1, l_x1);
		} else {
			l_shape = ShapeUtils.createTranslatedShape(l_shape, l_x1, l_y1);
		}
		if (l_shape.intersects(x_dataArea)) {
			x_graphics.setPaint(getItemPaint(x_series, x_item));
			x_graphics.fill(l_shape);
		}
		l_entityArea = l_shape;
	}

	// add an entity for the item...
	if (null != l_entities) {
		if (null == l_entityArea) {
			l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2), 4, 4);
		}
		String l_tip = null;
		XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series, x_item);
		if (null != l_tipGenerator) {
			l_tip = l_tipGenerator.generateToolTip(x_dataset, x_series, x_item);
		}
		String l_url = null;
		XYURLGenerator l_urlGenerator = getURLGenerator();
		if (null != l_urlGenerator) {
			l_url = l_urlGenerator.generateURL(x_dataset, x_series, x_item);
		}
		XYItemEntity l_entity = new XYItemEntity(l_entityArea, x_dataset, x_series, x_item, l_tip, l_url);
		l_entities.add(l_entity);
	}

	// draw the item label if there is one...
	if (isItemLabelVisible(x_series, x_item)) {
		drawItemLabel(x_graphics, l_orientation, x_dataset, x_series, x_item, l_x1, l_y1, (l_y1 < 0.0));
	}

	int datasetIndex = x_plot.indexOf(x_dataset);
	updateCrosshairValues(x_crosshairState, l_x0, l_y0, datasetIndex, l_x1, l_y1, l_orientation);

	if (0 == x_item) {
		return;
	}

	double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series, (x_item - 1)), x_dataArea,
			l_domainAxisLocation);
	double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, (x_item - 1)), x_dataArea,
			l_rangeAxisLocation);

	Line2D l_line = null;
	if (PlotOrientation.HORIZONTAL == l_orientation) {
		l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2);
	} else if (PlotOrientation.VERTICAL == l_orientation) {
		l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2);
	}

	if ((null != l_line) && l_line.intersects(x_dataArea)) {
		x_graphics.setPaint(getItemPaint(x_series, x_item));
		x_graphics.setStroke(getItemStroke(x_series, x_item));
		x_graphics.draw(l_line);
	}
}

//......... } `

b2405 avatar Feb 12 '21 21:02 b2405