jfreechart
jfreechart copied to clipboard
XYDifferenceRenderer: wrong paint switch with 'null' values
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
Case b) correct switch positive / negative paint
Case c) incorrect switch positive / negative paint
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?
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.
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
`/** * */ 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);
}
}
//......... } `