Menu

#793 Here is fix for StackedXYAreaRenderer2 w/ pos and neg values

open
nobody
None
5
2012-11-27
2007-09-05
David B
No

[also posted to forum at http://www.jfree.org/phpBB2/viewtopic.php?p=63973\]

The following code is adapted from StackedXYAreaRenderer2 from version 1.0.6. It fixes rendering bugs caused by series containing both positive and negative values. The bug was inappropriate gaps (whitespace) between the series polygons (when preceding series contains adjacent items of different signs), where these polygons should stack adjacently. If the stewards of JFreeChart wish to incorporate this code into JFreeChart, you are free to do so -- for this purpose I waive any copyright or license claims.

package org.jfree.chart.renderer.xy;

import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;

import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.labels.XYToolTipGenerator;
import org.jfree.chart.plot.CrosshairState;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYAreaRenderer2;
import org.jfree.chart.renderer.xy.XYItemRendererState;
import org.jfree.chart.urls.XYURLGenerator;
import org.jfree.data.Range;
import org.jfree.data.xy.TableXYDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.ui.RectangleEdge;
import org.jfree.util.PublicCloneable;

class StackedXYAreaRenderer3 extends XYAreaRenderer2 implements Cloneable, PublicCloneable, Serializable
{
/** For serialization. */
private static final long serialVersionUID = 7752676509764539182L;

/**
* 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).
*
* @since 1.0.3
*/
private boolean roundXCoordinates;

/**
* Creates a new renderer.
*/
public StackedXYAreaRenderer3() {
this(null, null);
}

/**
* Constructs a new renderer.
*
* @param labelGenerator the tool tip generator to use. <code>null</code>
* is none.
* @param urlGenerator the URL generator (<code>null</code> permitted).
*/
public StackedXYAreaRenderer3(XYToolTipGenerator labelGenerator,
XYURLGenerator urlGenerator) {
super(labelGenerator, urlGenerator);
this.roundXCoordinates = true;
}

/**
* Returns the flag that controls whether or not the x-coordinates (in
* Java2D space) are rounded to integer values.
*
* @return The flag.
*
* @since 1.0.4
*
* @see #setRoundXCoordinates(boolean)
*/
public boolean getRoundXCoordinates() {
return this.roundXCoordinates;
}

/**
* Sets the flag that controls whether or not the x-coordinates (in
* Java2D space) are rounded to integer values, and sends a
* {@link RendererChangeEvent} to all registered listeners.
*
* @param round the new flag value.
*
* @since 1.0.4
*
* @see #getRoundXCoordinates()
*/
public void setRoundXCoordinates(boolean round) {
this.roundXCoordinates = round;
notifyListeners(new RendererChangeEvent(this));
}

/**
* Returns the range of values the renderer requires to display all the
* items from the specified dataset.
*
* @param dataset the dataset (<code>null</code> permitted).
*
* @return The range (or <code>null</code> if the dataset is
* <code>null</code> or empty).
*/
public Range findRangeBounds(XYDataset dataset) {
if (dataset == null) {
return null;
}
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
TableXYDataset d = (TableXYDataset) dataset;
int itemCount = d.getItemCount();
for (int i = 0; i < itemCount; i++) {
double[] stackValues = getStackValues((TableXYDataset) dataset,
d.getSeriesCount(), i);
min = Math.min(min, stackValues[0]);
max = Math.max(max, stackValues[1]);
}
if (min == Double.POSITIVE_INFINITY) {
return null;
}
return new Range(min, max);
}

/**
* Returns the number of passes required by the renderer.
*
* @return 1.
*/
public int getPassCount() {
return 1;
}

/**
* Draws the visual representation of a single data item.
*
* @param g2 the graphics device.
* @param state the renderer state.
* @param dataArea the area within which the data is being drawn.
* @param info collects information about the drawing.
* @param plot the plot (can be used to obtain standard color information
* etc).
* @param domainAxis the domain axis.
* @param rangeAxis the range axis.
* @param dataset the dataset.
* @param series the series index (zero-based).
* @param item the item index (zero-based).
* @param crosshairState information about crosshairs on a plot.
* @param pass the pass index.
*/
public void drawItem(Graphics2D g2,
XYItemRendererState state,
Rectangle2D dataArea,
PlotRenderingInfo info,
XYPlot plot,
ValueAxis domainAxis,
ValueAxis rangeAxis,
XYDataset dataset,
int series,
int item,
CrosshairState crosshairState,
int pass) {

// setup for collecting optional entity info...
Shape entityArea = null;
EntityCollection entities = null;
if (info != null) {
entities = info.getOwner().getEntityCollection();
}

TableXYDataset tdataset = (TableXYDataset) dataset;

// get the data point...
double x1 = dataset.getXValue(series, item);
double y1 = dataset.getYValue(series, item);
if (Double.isNaN(y1)) {
y1 = 0.0;
}

// get the previous point and the next point so we can calculate a
// "hot spot" for the area (used by the chart entity)...
double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
if (Double.isNaN(y0)) {
y0 = 0.0;
}

int itemCount = dataset.getItemCount(series);
double x2 = dataset.getXValue(series, Math.min(item + 1, itemCount - 1));
double y2 = dataset.getYValue(series, Math.min(item + 1, itemCount - 1));
if (Double.isNaN(y2)) {
y2 = 0.0;
}

double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1, 0), false);

double[] stack1 = getStackValues(tdataset, series, item);
double[] stack1l = getStackValues(tdataset, series, item, true);
double[] stack1r = getStackValues(tdataset, series, item, false);

double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1, itemCount - 1), true);

double xleft = (x0 + x1) / 2.0;
double xright = (x1 + x2) / 2.0;
double[] stackLeft = averageStackValues(stack0, stack1l);
double[] stackRight = averageStackValues(stack1r, stack2);
double[] adjStackLeft = adjustedStackValues(stack0, stack1l);
double[] adjStackRight = adjustedStackValues(stack1r, stack2);

RectangleEdge edge0 = plot.getDomainAxisEdge();

float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0);
float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea, edge0);
float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea, edge0);

if (this.roundXCoordinates) {
transX1 = Math.round(transX1);
transXLeft = Math.round(transXLeft);
transXRight = Math.round(transXRight);
}
float transY1;
float transY1l;
float transY1r;

RectangleEdge edge1 = plot.getRangeAxisEdge();

GeneralPath left = new GeneralPath();
GeneralPath right = new GeneralPath();
if (y1 >= 0.0) { // handle positive value
transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, edge1);
transY1l = (float) rangeAxis.valueToJava2D(y1 + stack1l[1], dataArea, edge1);
transY1r = (float) rangeAxis.valueToJava2D(y1 + stack1r[1], dataArea, edge1);
float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], dataArea, edge1);
float transStack1l = (float) rangeAxis.valueToJava2D(stack1l[1], dataArea, edge1);
float transStack1r = (float) rangeAxis.valueToJava2D(stack1r[1], dataArea, edge1);
float transStackLeft = (float) rangeAxis.valueToJava2D(adjStackLeft[1], dataArea, edge1);

// LEFT POLYGON
if (y0 >= 0.0) {
double yleft = (y0 + y1) / 2.0 + stackLeft[1];
float transYLeft = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
left.moveTo(transX1, transY1);
left.lineTo(transX1, transStack1);
left.lineTo(transXLeft, transStackLeft);
left.lineTo(transXLeft, transYLeft);
left.closePath();
}
else {
left.moveTo(transX1, transY1);
left.lineTo(transX1, transStack1);
left.lineTo(transXLeft, transStackLeft);
left.closePath();
}

float transStackRight = (float) rangeAxis.valueToJava2D(adjStackRight[1], dataArea, edge1);
// RIGHT POLYGON
if (y2 >= 0.0) {
double yright = (y1 + y2) / 2.0 + stackRight[1];
float transYRight = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
right.moveTo(transX1, transY1);
right.lineTo(transX1, transStack1);
right.lineTo(transXRight, transStackRight);
right.lineTo(transXRight, transYRight);
right.closePath();
}
else {
right.moveTo(transX1, transY1);
right.lineTo(transX1, transStack1);
right.lineTo(transXRight, transStackRight);
right.closePath();
}
}
else { // handle negative value
transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea, edge1);
transY1l = (float) rangeAxis.valueToJava2D(y1 + stack1l[0], dataArea, edge1);
transY1r = (float) rangeAxis.valueToJava2D(y1 + stack1r[0], dataArea, edge1);
float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], dataArea, edge1);
float transStack1l = (float) rangeAxis.valueToJava2D(stack1l[0], dataArea, edge1);
float transStack1r = (float) rangeAxis.valueToJava2D(stack1r[0], dataArea, edge1);
float transStackLeft = (float) rangeAxis.valueToJava2D(adjStackLeft[0], dataArea, edge1);

// LEFT POLYGON
if (y0 >= 0.0) {
left.moveTo(transX1, transY1);
left.lineTo(transX1, transStack1);
left.lineTo(transXLeft, transStackLeft);
left.closePath();
}
else {
double yleft = (y0 + y1) / 2.0 + stackLeft[0];
float transYLeft = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
left.moveTo(transX1, transY1);
left.lineTo(transX1, transStack1);
left.lineTo(transXLeft, transStackLeft);
left.lineTo(transXLeft, transYLeft);
left.closePath();
}
float transStackRight = (float) rangeAxis.valueToJava2D(adjStackRight[0], dataArea, edge1);

// RIGHT POLYGON
if (y2 >= 0.0) {
right.moveTo(transX1, transY1);
right.lineTo(transX1, transStack1);
right.lineTo(transXRight, transStackRight);
right.closePath();
}
else {
double yright = (y1 + y2) / 2.0 + stackRight[0];
float transYRight = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
right.moveTo(transX1, transStack1);
right.lineTo(transX1, transY1);
right.lineTo(transXRight, transYRight);
right.lineTo(transXRight, transStackRight);
right.closePath();
}
}

// Get series Paint and Stroke
Paint itemPaint = getItemPaint(series, item);
if (pass == 0) {
g2.setPaint(itemPaint);
g2.fill(left);
g2.fill(right);
}

// add an entity for the item...
if (entities != null) {
GeneralPath gp = new GeneralPath(left);
gp.append(right, false);
entityArea = gp;
addEntity(entities, entityArea, dataset, series, item, transX1, transY1);
}

}

/**
* Calculates the stacked values (one positive and one negative) of all
* series up to, but not including, <code>series</code> for the specified
* item. It returns [0.0, 0.0] if <code>series</code> is the first series.
*
* @param dataset the dataset (<code>null</code> not permitted).
* @param series the series index.
* @param index the item index.
*
* @return An array containing the cumulative negative and positive values
* for all series values up to but excluding <code>series</code>
* for <code>index</code>.
*/
private double[] getStackValues(TableXYDataset dataset,
int series, int index) {
double[] result = new double[2];
for (int i = 0; i < series; i++) {
double v = dataset.getYValue(i, index);
if (!Double.isNaN(v)) {
if (v >= 0.0) {
result[1] += v;
}
else {
result[0] += v;
}
}
}
return result;
}

private double[] getStackValues(TableXYDataset dataset,
int series, int index, boolean left) {
double[] result = new double[2];
int itemCount = dataset.getItemCount();
for (int i = 0; i < series; i++) {
double v = dataset.getYValue(i, index);
double vv;
int index2;
if (left) {
index2 = Math.max(index-1, 0);
}
else {
index2 = Math.min(index+1, itemCount-1);
}
vv = dataset.getYValue(i, index2);
if (Math.signum(v) != Math.signum(vv)) {

v = 0.0;
}
if (!Double.isNaN(v)) {
if (v >= 0.0) {
result[1] += v;
}
else {
result[0] += v;
}
}
}
return result;
}

/**
* Returns a pair of "stack" values calculated as the mean of the two
* specified stack value pairs.
*
* @param stack1 the first stack pair.
* @param stack2 the second stack pair.
*
* @return A pair of average stack values.
*/
private double[] averageStackValues(double[] stack1, double[] stack2) {
double[] result = new double[2];
result[0] = (stack1[0] + stack2[0]) / 2.0;
result[1] = (stack1[1] + stack2[1]) / 2.0;
return result;
}

/**
* Calculates adjusted stack values from the supplied values. The value is
* the mean of the supplied values, unless either of the supplied values
* is zero, in which case the adjusted value is zero also.
*
* @param stack1 the first stack pair.
* @param stack2 the second stack pair.
*
* @return A pair of average stack values.
*/
private double[] adjustedStackValues(double[] stack1, double[] stack2) {
double[] result = new double[2];
if (stack1[0] == 0.0 || stack2[0] == 0.0) {
result[0] = 0.0;
}
else {
result[0] = (stack1[0] + stack2[0]) / 2.0;
}
if (stack1[1] == 0.0 || stack2[1] == 0.0) {
result[1] = 0.0;
}
else {
result[1] = (stack1[1] + stack2[1]) / 2.0;
}
return result;
}

/**
* Tests this renderer for equality with an arbitrary object.
*
* @param obj the object (<code>null</code> permitted).
*
* @return A boolean.
*/
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof StackedXYAreaRenderer3)) {
return false;
}
StackedXYAreaRenderer3 that = (StackedXYAreaRenderer3) obj;
if (this.roundXCoordinates != that.roundXCoordinates) {
return false;
}
return super.equals(obj);
}

/**
* Returns a clone of the renderer.
*
* @return A clone.
*
* @throws CloneNotSupportedException if the renderer cannot be cloned.
*/
public Object clone() throws CloneNotSupportedException {
return super.clone();
}

}

Discussion


Log in to post a comment.