Code: Select all
package org.jfree.chart.axis;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTick;
import org.jfree.chart.axis.TickType;
import org.jfree.chart.event.AxisChangeEvent;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.ValueAxisPlot;
import org.jfree.data.Range;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.TextAnchor;
/**
* <p>
* A normal probability axis can be used in statistical analysis to show how
* closely a set of data relates to a normal, or log-normal, distribution. The
* tick marks along the axis are cumulative percent of a normal distribution.
* The tick marks are scaled so that the number of standard deviations from the
* mean of a normal (Gaussian) distribution are transformed into percent of the
* cumulative normal distribution. The number of standard deviations would be a
* linear axis. The value of the data is the cumulative percent of a normal
* distribution. The data in java2d space is the number of standard deviations
* from the median of the distribution.
* </p>
* <p>
*
* If the other axis is linear, and the data plots as a straight line, then the
* data is normally distributed. If the other axis is logarithmic, and the data
* plots as a straight line, then the data is log-normally distributed. Both
* axes in a graph cannot logically be probability axes.
* </p>
* <p>
* Several methods are adaptations of the methods in the org.jfree.axis.LogAxis
* and LogarithmicAxis classes.
* </p>
* <p>
*
* @author John St. Ledger
* @version 1.0 09/30/2011
*/
public class ProbabilityAxis extends NumberAxis implements Cloneable,
Serializable
{
/**
* Holds all of the major and minor NumberTick objects that are allowed on a
* probability axis.
*/
private Map<Double, NumberTick> probTickMarks = new Hashtable<Double, NumberTick>(
40);
/** The smallest value permitted on the axis. */
private double smallestValue = 0.5;
/** The smallest value permitted on the axis. */
private double largestValue = 99.5;
/** For serialization. */
private static final long serialVersionUID = 2805933088476185790L;
/**
* Default constructor.
*/
public ProbabilityAxis()
{
this(null);
}
/**
* Constructs a number axis, using default values where necessary.
*
* @param label
* the axis label (<code>null</code> permitted).
*/
public ProbabilityAxis(String label)
{
super(label);
Range range = new Range(smallestValue, largestValue);
setDefaultAutoRange(range);
setRange(range);
setFixedAutoRange(largestValue - smallestValue);
createProbTicks(TextAnchor.TOP_CENTER);
this.setMinorTickMarksVisible(true);
}
/**
* <p>
* This method calculates a polynomial approximation for the cumulative
* normal function. This is the integral of the normal distribution function
* from minus infinity to z.
* </p>
*
* <p>
* ONE MAJOR DIFFERENCE: This returns the CNF as a percentage, rather than
* as a fraction. A fraction of 0.5 returns 50. The method is static so that
* it can be called by other classes to help calculate the data to be
* plotted.
* </p>
*
* <ul>
* Reference:
* <li>Abramowitz, Milton and Irene Stegun, <i>Handbook of Mathematical
* Functions, with Formulas, Graphs, and Mathematical Tables</i>, Government
* Printing Office, National Bureau of Standards, 10th Edition, December
* 1972, Page 932.</li>
* </ul>
*
* <p>
* Verified by comparison to MathCad cnorm calculations. Verified accurate
* to 1.5E-07 with a JUnit test
* </p>
*
* <p>
* author John St. Ledger, 11/01/99
* </p>
*
* @see #calculateInvCNF(double)
*
* @param z
* The number of standard deviations. It is (x - meanx)/sigma.
* The mean is the average of the distribution, the sigma is the
* standard deviation.
* @return The cumulative normal function value of the argument, as a
* percentage.
*/
public static double calculateCNF(double z)
{
/**
* Constants in the polynomial curve fit for the function.
*/
double[] d =
{ 1.0, 0.0498673470, 0.0211410061, 0.0032776263, 0.0000380036,
0.0000488906, 0.0000053830 };
/**
* Maximum number of standard deviations. Below the negative of this the
* method returns 0, above it returns 1.
*/
double zmax = 6;
/**
* The cumulative normal result.
*/
double anormal;
if (z <= -zmax)
{
anormal = 0.0;
}
else if (z >= zmax)
{
anormal = 1.0;
}
else
{
double x = Math.abs(z);
anormal = 1.0 - 0.5 / Math.pow(polyCalc(d, x), 16);
if (z < 0.0)
{
anormal = 1.0 - anormal;
}
}
return anormal * 100;
}
/**
* <p>
* Calculates the number of standard deviations from a given cumulative
* percentage of a normal distribution.
* </p>
* <p>
*
* This method is the inverse of the cumulative normal function. If you give
* it the cumulative percentage, it returns the number of standard
* deviations required to give that probability. For large percentages near
* 100, it returns 7, and for small values near 0 it returns -7.
* </p>
* <p>
* Verified accurate to within 4.5E-04 with a JUnit test, for -7.0 to 7.0
* standard deviations. The smallest value returned is -7, and the largest
* value returned is 7. The method is static so that it can be called by
* other classes to help calculate the data to be plotted.
* </p>
*
* <p>
* author John St. Ledger, 11/01/99
* </p>
*
* <ul>
* <li>Abramowitz, Milton and Irene Stegun, <i>Handbook of Mathematical
* Functions, with Formulas, Graphs, and Mathematical Tables</i>, Government
* Printing Office, National Bureau of Standards, 10th Edition, December
* 1972, Page 933.
* </ul>
*
* @param percent
* The cumulative probability in percent for a normal
* distribution, where the probability is the integral of the
* normal distribution from minus infinity to x, times 100.
* @return The value of x, when the input percentage is the integral of the
* normal distribution from minus infinity to x.
* @see #calculateCNF(double)
*/
public static double calculateInvCNF(double percent)
{
if (percent >= 099.99999999987201)
return 7;
else if (percent <= 127.98095916366492E-12)
return -7;
double p;
percent = percent / 100.0;
if (percent == 0.5)
return 0;
if (percent >= 0.5)
p = 1 - percent;
else
p = percent;
double t = Math.sqrt(Math.log(1.0 / (p * p)));
double[] c =
{ 2.515517, 0.802853, 0.010328 };
double[] d =
{ 1.0, 1.432788, 0.189269, 0.001308 };
double x = t - polyCalc(c, t) / polyCalc(d, t);
if (percent >= 0.5)
return x;
else
return -x;
}
/**
* This method creates a collection of number ticks that are allowed on a
* probability axis. The collection holds all of the major and minor
* NumberTick objects.
*/
private void createProbTicks(TextAnchor ta)
{
Iterator<Double> tmIterator = probTickMarks.keySet().iterator();
// First, remove all tick marks
while (tmIterator.hasNext())
{
tmIterator.next();
tmIterator.remove();
}
TextAnchor textAnchor = ta;
// Now add all of the tick marks back in
// Must do the remove and add, Number Ticks are immutable, so we can't
// change the
// textAnchor value. We could use an arrangement of arrays to keep track
// of the
// value and labels, but this way seems easier to understand.
probTickMarks.put(new Double(0.5), new NumberTick(TickType.MAJOR, 0.5,
"0.5", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(1.0), new NumberTick(TickType.MAJOR, 1.0,
"1", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(2.0), new NumberTick(TickType.MAJOR, 2,
"2", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(3.0), new NumberTick(TickType.MINOR, 3,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(4.0), new NumberTick(TickType.MINOR, 4,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(5.0), new NumberTick(TickType.MAJOR, 5,
"5", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(7.5), new NumberTick(TickType.MINOR, 7.5,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(10.0), new NumberTick(TickType.MAJOR, 10,
"10", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(15.0), new NumberTick(TickType.MINOR, 15,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(20.0), new NumberTick(TickType.MAJOR, 20,
"20", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(25.0), new NumberTick(TickType.MINOR, 25,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(30.0), new NumberTick(TickType.MAJOR, 30,
"30", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(35.0), new NumberTick(TickType.MINOR, 35,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(40.0), new NumberTick(TickType.MAJOR, 40,
"40", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(45.0), new NumberTick(TickType.MINOR, 45,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(50.0), new NumberTick(TickType.MAJOR, 50,
"50", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(55.0), new NumberTick(TickType.MINOR, 55,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(60.0), new NumberTick(TickType.MAJOR, 60,
"60", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(65.0), new NumberTick(TickType.MINOR, 65,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(70.0), new NumberTick(TickType.MAJOR, 70,
"70", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(75.0), new NumberTick(TickType.MINOR, 75,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(80.0), new NumberTick(TickType.MAJOR, 80,
"80", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(85.0), new NumberTick(TickType.MINOR, 85,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(90.0), new NumberTick(TickType.MAJOR, 90,
"90", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(92.5), new NumberTick(TickType.MINOR,
92.5, "", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(95.0), new NumberTick(TickType.MAJOR, 95,
"95", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(96.0), new NumberTick(TickType.MINOR, 96,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(97.0), new NumberTick(TickType.MINOR, 97,
"", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(98.0), new NumberTick(TickType.MAJOR, 98,
"98", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.0), new NumberTick(TickType.MAJOR, 99,
"99", textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.5), new NumberTick(TickType.MAJOR,
99.5, "99.5", textAnchor, TextAnchor.CENTER, 0.0));
}
/**
* Adjusts the axis range to match the data range that the axis is required
* to display.
*/
protected void autoAdjustRange()
{
Plot plot = getPlot();
if (plot == null)
{
return; // no plot, no data
}
if (plot instanceof ValueAxisPlot)
{
ValueAxisPlot vap = (ValueAxisPlot) plot;
Range r = vap.getDataRange(this);
if (r == null)
{
r = getDefaultAutoRange();
}
double upper = Math.min(r.getUpperBound(), largestValue);
double lower = Math.max(r.getLowerBound(), smallestValue);
double range = upper - lower;
// if fixed auto range, then derive lower bound...
double fixedAutoRange = getFixedAutoRange();
if (fixedAutoRange > 0.0)
{
lower = Math.max(upper - fixedAutoRange, smallestValue);
}
else
{
// ensure the autorange is at least <minRange> in size...
double minRange = getAutoRangeMinimumSize();
if (range < minRange)
{
double expand = (minRange - range) / 2;
upper = upper + expand;
lower = lower - expand;
}
// apply the margins - these should apply to the exponent range
double logUpper = calculateInvCNF(upper);
double logLower = calculateInvCNF(lower);
double logRange = logUpper - logLower;
logUpper = logUpper + getUpperMargin() * logRange;
logLower = logLower - getLowerMargin() * logRange;
upper = calculateCNF(logUpper);
lower = calculateCNF(logLower);
}
setRange(new Range(lower, upper), false, false);
}
}
/**
* Converts a data value to a coordinate in Java2D space, assuming that the
* axis runs along one edge of the specified dataArea. Given a percentage,
* find the Java2D coordinate value.
* <p>
* Note that it is possible for the coordinate to fall outside the plotArea.
*
* @param value
* the data value.
* @param area
* the area for plotting the data.
* @param edge
* the axis location.
*
* @return The Java2D coordinate.
*
* @see #java2DToValue(double, Rectangle2D, RectangleEdge)
*/
public double valueToJava2D(double value, Rectangle2D area,
RectangleEdge edge)
{
Range range = getRange();
double axisMin = calculateInvCNF(range.getLowerBound());
double axisMax = calculateInvCNF(range.getUpperBound());
value = calculateInvCNF(value);
double min = 0.0;
double max = 0.0;
if (RectangleEdge.isTopOrBottom(edge))
{
min = area.getX();
max = area.getMaxX();
}
else if (RectangleEdge.isLeftOrRight(edge))
{
max = area.getMinY();
min = area.getMaxY();
}
if (isInverted())
{
return max - ((value - axisMin) / (axisMax - axisMin))
* (max - min);
}
else
{
return min + ((value - axisMin) / (axisMax - axisMin))
* (max - min);
}
}
/**
* Converts a coordinate in Java2D space to the corresponding data (percent)
* value, assuming that the axis runs along one edge of the specified
* dataArea. So given a linearly spaced range in Java2D, this finds the
* linear value in standard deviations, and converts it to cumulative
* percent.
*
* @param java2DValue
* the coordinate in Java2D space.
* @param area
* the area in which the data is plotted.
* @param edge
* the location.
*
* @return The data value.
*
* @see #valueToJava2D(double, Rectangle2D, RectangleEdge)
*/
public double java2DToValue(double java2DValue, Rectangle2D area,
RectangleEdge edge)
{
Range range = getRange();
double axisMin = calculateInvCNF(range.getLowerBound());
double axisMax = calculateInvCNF(range.getUpperBound());
double min = 0.0;
double max = 0.0;
if (RectangleEdge.isTopOrBottom(edge))
{
min = area.getX();
max = area.getMaxX();
}
else if (RectangleEdge.isLeftOrRight(edge))
{
min = area.getMaxY();
max = area.getY();
}
if (isInverted())
{
return calculateCNF(axisMax - (java2DValue - min) / (max - min)
* (axisMax - axisMin));
}
else
{
return calculateCNF(axisMin + (java2DValue - min) / (max - min)
* (axisMax - axisMin));
}
}
/**
* Calculates the positions of the tick labels for the axis, storing the
* results in the tick label list (ready for drawing).
*
* @param g2
* the graphics device.
* @param dataArea
* the area in which the data should be drawn.
* @param edge
* the location of the axis.
*
* @return A list of ticks.
*/
protected List<NumberTick> refreshTicksHorizontal(Graphics2D g2,
Rectangle2D dataArea, RectangleEdge edge)
{
List<NumberTick> ticks = new ArrayList<NumberTick>();
Font tickLabelFont = getTickLabelFont();
g2.setFont(tickLabelFont);
TextAnchor textAnchor;
if (edge == RectangleEdge.TOP)
{
textAnchor = TextAnchor.BOTTOM_CENTER;
}
else
{
textAnchor = TextAnchor.TOP_CENTER;
}
createProbTicks(textAnchor);
double start = getLowerBound();
double end = getUpperBound();
Iterator<Double> tickIterator = probTickMarks.keySet().iterator();
while (tickIterator.hasNext())
{
Double key = tickIterator.next();
double percent = key.doubleValue();
if (percent >= start && percent <= end)
{
ticks.add(probTickMarks.get(key));
}
}
return ticks;
}
/**
* Calculates the positions of the tick labels for the axis, storing the
* results in the tick label list (ready for drawing).
*
* @param g2
* the graphics device.
* @param dataArea
* the area in which the plot should be drawn.
* @param edge
* the location of the axis.
*
* @return A list of ticks.
*/
protected List<NumberTick> refreshTicksVertical(Graphics2D g2,
Rectangle2D dataArea, RectangleEdge edge)
{
List<NumberTick> ticks = new ArrayList<NumberTick>();
Font tickLabelFont = getTickLabelFont();
g2.setFont(tickLabelFont);
TextAnchor textAnchor;
if (edge == RectangleEdge.RIGHT)
{
textAnchor = TextAnchor.CENTER_LEFT;
}
else
{
textAnchor = TextAnchor.CENTER_RIGHT;
}
createProbTicks(textAnchor);
double start = getLowerBound();
double end = getUpperBound();
Iterator<Double> tickIterator = probTickMarks.keySet().iterator();
while (tickIterator.hasNext())
{
Double key = tickIterator.next();
double percent = key.doubleValue();
if (percent >= start && percent <= end)
{
ticks.add(probTickMarks.get(key));
}
}
return ticks;
}
/**
* Returns a clone of the axis.
*
* @return A clone
*
* @throws CloneNotSupportedException
* if some component of the axis does not support cloning.
*/
public Object clone() throws CloneNotSupportedException
{
ProbabilityAxis clone = (ProbabilityAxis) super.clone();
return clone;
}
/**
* <p>
* Calculates a polynomial of degree n. The polynomial evaluation is of the
* form: p = sum{c(i)*x^i} As numerical recipes says, it is apparently very
* bad form to evaluate the polynomial in a straight forward fashion, so
* this method used the approach from page 167 of numerical recipes. All of
* the derivatives of the polynomial could be calculated from the method on
* page 168, if desired in the future.
* <p>
* Verified with a JUnit test.
* </p>
* <p>
* Using this approach, the error growth is a linear function of the degree
* of the polynomial.(Uses the Horner algorithm)
* </p>
*
* <ul>
* Reference:
* <li>Press, William H., et. al., <i>Numerical Recipes in Fortran 77,
* Second Edition, The Art of Scientific Computing</i>, Cambridge Los Alamos
* National Security, LLC Press, 1997, page 167, section 5.3.</li>
* </ul>
*
* <p>
* author John St. Ledger
* </p>
* <p>
* version 1.0, 12/14/2005
* </p>
*
* @param a
* The coefficients of the polynomial, ordered from coefficient 0
* to n. Must be at least size 1.
* @param x
* The value at which the polynomial is to be evaluated.
* @return The value of the polynomial.
*/
private static double polyCalc(double[] a, double x)
{
double answer;
answer = a[a.length - 1]; // the evaluated polynomial
for (int i = a.length - 2; i >= 0; i--)
{
answer = answer * x + a[i];
}
return answer;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
public int hashCode()
{
final int prime = 31;
int result = super.hashCode();
long temp;
temp = Double.doubleToLongBits(largestValue);
result = prime * result + (int) (temp ^ (temp >>> 32));
result = prime * result
+ ((probTickMarks == null) ? 0 : probTickMarks.hashCode());
temp = Double.doubleToLongBits(smallestValue);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (!(obj instanceof ProbabilityAxis))
return false;
ProbabilityAxis other = (ProbabilityAxis) obj;
if (Double.doubleToLongBits(largestValue) != Double
.doubleToLongBits(other.largestValue))
return false;
if (probTickMarks == null)
{
if (other.probTickMarks != null)
return false;
}
else if (!probTickMarks.equals(other.probTickMarks))
return false;
if (Double.doubleToLongBits(smallestValue) != Double
.doubleToLongBits(other.smallestValue))
return false;
return true;
}
/**
* Returns the smallest value represented by the axis.
*
* @return The smallest value represented by the axis.
*
* @see #setSmallestValue(double)
*/
public double getSmallestValue()
{
return this.smallestValue;
}
/**
* Returns the smallest value represented by the axis.
*
* @return The smallest value represented by the axis.
*
* @see #setSmallestValue(double)
*/
public double getLargestValue()
{
return this.largestValue;
}
/**
* Sets the smallest value represented by the axis and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param value
* the value.
*
* @see #getSmallestValue()
*/
public void setSmallestValue(double value)
{
if (value < 0.5)
{
value = 0.5;
}
this.smallestValue = value;
notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the smallest value represented by the axis and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param value
* the value.
*
* @see #getSmallestValue()
*/
public void setLargestValue(double value)
{
if (value > 99.5)
{
value = 99.5;
}
this.largestValue = value;
notifyListeners(new AxisChangeEvent(this));
}
/**
* Zooms in on the current range.
*
* @param lowerPercent
* the new lower bound.
* @param upperPercent
* the new upper bound.
*/
public void zoomRange(double lowerPercent, double upperPercent)
{
Range range = getRange();
double start = range.getLowerBound();
double end = range.getUpperBound();
double log1 = calculateInvCNF(start);
double log2 = calculateInvCNF(end);
double length = log2 - log1;
Range adjusted = null;
if (isInverted())
{
double logA = log1 + length * (1 - upperPercent);
double logB = log1 + length * (1 - lowerPercent);
adjusted = new Range(calculateCNF(logA), calculateCNF(logB));
}
else
{
double logA = log1 + length * lowerPercent;
double logB = log1 + length * upperPercent;
adjusted = new Range(calculateCNF(logA), calculateCNF(logB));
}
setRange(adjusted);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return "ProbabilityAxis [smallestValue = " + smallestValue
+ ", largestValue = " + largestValue + "]";
}
}