## New Axis Type ProbabilityAxis

A discussion forum for JFreeChart (a 2D chart library for the Java platform).
stledger
Posts: 14
Joined: Sun Mar 09, 2008 7:39 am

### New Axis Type ProbabilityAxis

This class generates a probability axis. See the comments for its use.

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.
*/
{
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)
{
}
}
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)
{
}
}
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)
{

answer = a[a.length - 1]; // the evaluated polynomial

for (int i = a.length - 2; i >= 0; i--)
{
}
}

/*
*
* @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;
}

/*
*
* @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;
if (isInverted())
{
double logA = log1 + length * (1 - upperPercent);
double logB = log1 + length * (1 - lowerPercent);
}
else
{
double logA = log1 + length * lowerPercent;
double logB = log1 + length * upperPercent;
}
}

/*
*
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return "ProbabilityAxis [smallestValue = " + smallestValue
+ ", largestValue = " + largestValue + "]";
}
}
``````
John

stledger
Posts: 14
Joined: Sun Mar 09, 2008 7:39 am

### Re: New Axis Type ProbabilityAxis

I made some minor changes to the probability axis. Extended the range, and took out some magic numbers. This is free code donated to the JFreeChart library.

Code: Select all

``````package org.jfree.chart.axis;
/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
*
* Project Info:  http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
*/

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
*
* @version 1.1    02/01/2014
* <p>Extended the maximum limits axis range to 0.1 to 99.9 percent. Lowest and largest axis limits
* are now final values, and not "magic" numbers.
*/
public class ProbabilityAxis extends NumberAxis implements Cloneable, Serializable
{

/** Holds all of the major and minor NumberTick objects that
*  are allowed on a probability axis. The load factor is 0.75.
*/
private Map <Double, NumberTick> probTickMarks = new Hashtable <Double, NumberTick> (54);

/** The absolute lowest value on a probability axis */
private final double lowerLimit = 0.1;

/** The absolute highest value on a probability axis */
private final double upperLimit = 99.9;

/**  The default smallest value showing on the axis. */
private double smallestValue = lowerLimit;

/**  The default largest value showing on the axis. */
private double largestValue = upperLimit;

/** For serialization. */
private static final long serialVersionUID = 2805933088476185791L;

/**
* 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.1), new NumberTick(TickType.MAJOR, 0.1, "0.1",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.2), new NumberTick(TickType.MINOR, 0.2, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.3), new NumberTick(TickType.MINOR, 0.3, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.4), new NumberTick(TickType.MINOR, 0.4, "",
textAnchor, TextAnchor.CENTER, 0.0));
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));
probTickMarks.put(new Double(99.6), new NumberTick(TickType.MINOR, 99.6, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.7), new NumberTick(TickType.MINOR, 99.7, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.8), new NumberTick(TickType.MINOR, 99.8, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.9), new NumberTick(TickType.MAJOR,  99.9, "99.9",
textAnchor, TextAnchor.CENTER, 0.0));
}

/**
* Adjusts the axis range to match the data range that the axis is
* required to display.
*/
{
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)
{
}
}
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)
{
}
}
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)
{

answer = a[a.length - 1];    // the evaluated polynomial

for(int i = a.length - 2; i >= 0; i--)
{
}
}

* @see java.lang.Object#hashCode()
*/
@Override
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;
}

* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) 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 < lowerLimit)
{
value = lowerLimit;
}
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 > upperLimit)
{
value = upperLimit;
}
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;
if (isInverted()) {
double logA = log1 + length * (1 - upperPercent);
double logB = log1 + length * (1 - lowerPercent);
}
else {
double logA = log1 + length * lowerPercent;
double logB = log1 + length * upperPercent;
}
}

* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return "ProbabilityAxis [smallestValue = " + smallestValue + ", largestValue = " + largestValue + "]";
}
}
``````

david.gilbert
Posts: 11707
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

### Re: New Axis Type ProbabilityAxis

Hi John,

This is really nice! The only problem I noticed is when zooming in, once the fixed tick labels are out of range it is no longer clear what the x-axis is showing. I don't know if you have any suggestions to fix that, but I'd like to try to find a solution for the problem (I guess at the very least to label the end points of the axis when there are no other labels).
David Gilbert Read my blog Ask your company to buy the JFreeChart Developer Guide Check out other products sold by my company Object Refinery Limited

stledger
Posts: 14
Joined: Sun Mar 09, 2008 7:39 am

### Re: New Axis Type ProbabilityAxis

David,

I hadn't even considered that. I think I see a way to fix it so that tick marks show on zoom in, but I won't be able to work on it for a few weeks. I'll submit a new version around April 15.

John

stledger
Posts: 14
Joined: Sun Mar 09, 2008 7:39 am

### Re: New Axis Type ProbabilityAxis

I have fixed the axis so that at least one labeled tick mark will show on zoom in. I've limited the minimum axis range length to 0.1, and the smallest tick mark step size to 0.01. This is due to the accuracy of the approximations for calculating the cumulative normal function and inverse cumulative normal function. Zooming in more than these levels, leads to the display and tick marks being inaccurate. I have approximations that are more accurate (to about 1E-13), but I don't think they are necessary for using the axis. On zoom out, the axis may display values less than the lower limit, or more than the upper limit. Again, I don't think this is a problem in using the axis for analyzing data that is plotted with the axis. If you wish, I can add the more accurate approximations. It's easy, I just don't think it's needed. It would add a lot more lines of code. Let me know if you agree or not.

Code: Select all

``````package org.jfree.chart.axis;
/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
*
* Project Info:  http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
*/

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

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
*
* @version 1.1    02/01/2014
* <p>Extended the maximum limits axis range to 0.1 to 99.9 percent. Lowest and largest axis limits
* are now final values, and not "magic" numbers.</p><p>
*
* @version 1.2    04/19/2014
* <p>On a zoom in, the axis will now show at least one major tick mark with a label.
* The zoom in is limited to a minimum range length of 0.1. If the zoom is allowed to
* zoom in to a smaller overall length, inaccuracies in the plot and tick marks occur,
* because the accuracy of the calculateInvCNF method is limited to 4.5E-02. If the
* calculateInvCNF method is replaced with a method accurate to about 1E-13, then the
* zoom fails at a length of about 0.0001, because the accuracy of the calculateCNF
* method is 1E-05. On zoom out, the axis may have values less than the lower limit
* or greater than the upper limit, but the tick marks will not extend past the limits.</p><p>
*/
public class ProbabilityAxis extends NumberAxis implements Cloneable, Serializable
{

/** Holds all of the major and minor NumberTick objects that
*  are allowed on a probability axis. The load factor is 0.75.
*/
private Map <Double, NumberTick> probTickMarks = new Hashtable <Double, NumberTick> (54);

/** The absolute lowest value on a probability axis */
private final double lowerLimit = 0.1;

/** The absolute highest value on a probability axis */
private final double upperLimit = 99.9;

/** The minimum length allowed for a probability axis */
private final double minLength = 0.1;

/**  The default smallest value showing on the axis. */
private double smallestValue = lowerLimit;

/**  The default largest value showing on the axis. */
private double largestValue = upperLimit;

/** For serialization. */
private static final long serialVersionUID = 2805933088476185791L;

/**
* 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-05 as percent 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-02 of percent 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;
Range range = getRange();
double deltaRange = range.getLength();
// Now add all of the tick marks back in

if(deltaRange >= 10.0)
{
// 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.1), new NumberTick(TickType.MAJOR, 0.1, "0.1",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.2), new NumberTick(TickType.MINOR, 0.2, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.3), new NumberTick(TickType.MINOR, 0.3, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.4), new NumberTick(TickType.MINOR, 0.4, "",
textAnchor, TextAnchor.CENTER, 0.0));
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));
probTickMarks.put(new Double(99.6), new NumberTick(TickType.MINOR, 99.6, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.7), new NumberTick(TickType.MINOR, 99.7, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.8), new NumberTick(TickType.MINOR, 99.8, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.9), new NumberTick(TickType.MAJOR,  99.9, "99.9",
textAnchor, TextAnchor.CENTER, 0.0));
}
else // on zoom in, use evenly spaced tick marks in percentage space and all major ticks
{
double minRange = range.getLowerBound();
double trialRange = 2.0;

while(true)
{
if(trialRange <= deltaRange)
{
break;
}
else
{
trialRange = trialRange/10.0;
}
}

double tickSize = trialRange/2.0;
int exponent = Math.abs((int) Math.log10(tickSize));
tickSize = setPrecision(tickSize, exponent);

double minTick = setPrecision(minRange, exponent);
// a right click on the chart panel, and selecting Zoom Out can zoom the
// axis limits beyond the lower limit. This prevents a tick mark being
// shown for values < the lowerLimit
if(minTick < lowerLimit) minTick = lowerLimit;

NumberFormat form = NumberFormat.getInstance();
form.setMaximumFractionDigits((exponent));
form.setMinimumFractionDigits((exponent));

for(int i=0; i<21; i++)
{
Double tickVal = new Double(minTick + i*tickSize);
// a right click on the chart panel, and selecting Zoom Out can zoom the
// axis limits beyond the upper limit. This prevents a tick mark being
// shown for values > the upperLimit
if(tickVal.doubleValue() > upperLimit) break;
probTickMarks.put(tickVal, new NumberTick(TickType.MAJOR,  tickVal.doubleValue(),
form.format(tickVal.doubleValue()), textAnchor, TextAnchor.CENTER, 0.0));
}
}
}

/**
* Adjusts the axis range to match the data range that the axis is
* required to display.
*/
{
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;
}

// This section limits the zoom in to a length of 0.1. This limit cannot be
// extended unless the caclulateCNF and calculateInvCNF methods are replaced
// with more accurate methods. The minimum range length is about the accuracy
// of these methods, for the zoom in to show accurate tick marks.
double length = getRange().getLength();
if(length < minLength)
{
double center = getRange().getCentralValue();
Range range = new Range(center - minLength/2.0, center + minLength/2.0);
if(range.getUpperBound() > upperLimit)
{
range = new Range(upperLimit - minLength, upperLimit);
}
else if(range.getLowerBound() < lowerLimit)
{
range = new Range(lowerLimit, lowerLimit + minLength);
}
setRange(range);
}

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)
{
}
}
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;
}

// This section limits the zoom in to a length of 0.1. This limit cannot be
// extended unless the caclulateCNF and calculateInvCNF methods are replaced
// with more accurate methods. The minimum range length is about the accuracy
// of these methods, for the zoom in to show accurate tick marks.
double length = getRange().getLength();
if(length < minLength)
{
double center = getRange().getCentralValue();
Range range = new Range(center - minLength/2.0, center + minLength/2.0);
if(range.getUpperBound() > upperLimit)
{
range = new Range(upperLimit - minLength, upperLimit);
}
else if(range.getLowerBound() < lowerLimit)
{
range = new Range(lowerLimit, lowerLimit + minLength);
}
setRange(range);
}

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)
{
}
}
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)
{

answer = a[a.length - 1];    // the evaluated polynomial

for(int i = a.length - 2; i >= 0; i--)
{
}
}

* @see java.lang.Object#hashCode()
*/
@Override
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;
}

* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) 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 < lowerLimit)
{
value = lowerLimit;
}
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 > upperLimit)
{
value = upperLimit;
}
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;
if (isInverted()) {
double logA = log1 + length * (1 - upperPercent);
double logB = log1 + length * (1 - lowerPercent);
}
else {
double logA = log1 + length * lowerPercent;
double logB = log1 + length * upperPercent;
}
}

* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return "ProbabilityAxis [smallestValue = " + smallestValue + ", largestValue = " + largestValue + "]";
}

/**<p>
*  This method takes a decimal number, and rounds it so that only the desired number
*  of digits to the right of the decimal point are retained. So an entry of
*  (1.052678, 3) would return 1.053. An entry of (0.6652248, 3) would return
*  0.665. Negative values for the number of decimal places will convert digits to
*  the left of the decimal point to 0, so that an entry of (12345.0, -3) will
*  return 12000.0.</p><p>
*
*  Verified with JUnit test.</p><p>
*
*  <b>CAUTION: </b> This method will only work for inputs that are less than the
*  largest 64 bit long, or about 9.2E16. There is roundoff error with only 16 digits
*  represented in the double representation, so the exceptions are actually thrown
*  for numbers larger than 2^64, rather than 2^63 - 1.</p>
*
*   <p>author  John St. Ledger, 11/01/99</p>
*
*  @throws  IllegalArgumentException if the input value is greater than Long.MAX_VAlUE,
*                                    or less than LONG.MIN_VALUE.
*
* @param value          The value to truncate and round the number of decimals.
* @param numberPlaces   The number of decimal places to retain to the right of the decimal.
* @return     The input value rounded and then truncated to the desired precision.
*/
static public double setPrecision(double value, int numberPlaces)
{
if(value > Long.MAX_VALUE) throw new IllegalArgumentException("ProbabilityAxis.setPrecision() argument is too large");
if(value < Long.MIN_VALUE) throw new IllegalArgumentException("ProbabilityAxis.setPrecision() argument is too small");
double size = Math.pow(10, numberPlaces);
return Math.round(value*size)/size;
}

}
``````
John

stledger
Posts: 14
Joined: Sun Mar 09, 2008 7:39 am

### Re: New Axis Type ProbabilityAxis

I had a bug in the previous submittal. If you zoomed out and then back in around the 0.1 or 99.9 tick marks on the axis, the tick marks were not always in the correct position, and you could get 0 or 100 for the tick mark, etc. I have this fixed now. I also fixed the setPrecision() method so that it is more correct.

Code: Select all

``````package org.jfree.chart.axis;
/* ===========================================================
* JFreeChart : a free chart library for the Java(tm) platform
* ===========================================================
*
* (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
*
* Project Info:  http://www.jfree.org/jfreechart/index.html
*
* This library is free software; you can redistribute it and/or modify it
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
* USA.
*
* [Java is a trademark or registered trademark of Sun Microsystems, Inc.
* in the United States and other countries.]
*
*/

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.text.NumberFormat;
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
*
* @version 1.1    02/01/2014
* <p>Extended the maximum limits axis range to 0.1 to 99.9 percent. Lowest and largest axis limits
* are now final values, and not "magic" numbers.</p><p>
*
* @version 1.2.1    04/25/2014
* <p>On a zoom in, the axis will now show at least one major tick mark with a label.
* The zoom in is limited to a minimum range length of 0.1. If the zoom is allowed to
* zoom in to a smaller overall length, inaccuracies in the plot and tick marks occur,
* because the accuracy of the calculateInvCNF method is limited to 4.5E-02. If the
* calculateInvCNF method is replaced with a method accurate to about 1E-13, then the
* zoom fails at a length of about 0.0001, because the accuracy of the calculateCNF
* method is 1E-05. On zoom out, the axis may have values less than the lower limit
* or greater than the upper limit, but the tick marks will not extend past the limits.</p><p>
*/
public class ProbabilityAxis extends NumberAxis implements Cloneable, Serializable
{

/** Holds all of the major and minor NumberTick objects that
*  are allowed on a probability axis. The load factor is 0.75.
*/
private Map <Double, NumberTick> probTickMarks = new Hashtable <Double, NumberTick> (55);

/** The absolute lowest tick mark value on a probability axis */
private final double lowerLimit = 0.1;

/** The absolute highest tick mark value on a probability axis */
private final double upperLimit = 99.9;

/** The minimum length allowed for a probability axis */
private final double minLength = 0.1;

/**  The default smallest value showing on the axis. */
private double smallestValue = lowerLimit;

/**  The default largest value showing on the axis. */
private double largestValue = upperLimit;

/** For serialization. */
private static final long serialVersionUID = 2805933088476185791L;

/**
* 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-05 as percent 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-02 of percent 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.
*
*  @param ta  The text anchor for the tick marks
*/
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;
Range range = getRange();
double deltaRange = range.getLength();
// Now add all of the tick marks back in

if(deltaRange >= 10.0)
{
// 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.1), new NumberTick(TickType.MAJOR, 0.1, "0.1",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.2), new NumberTick(TickType.MINOR, 0.2, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.3), new NumberTick(TickType.MINOR, 0.3, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(0.4), new NumberTick(TickType.MINOR, 0.4, "",
textAnchor, TextAnchor.CENTER, 0.0));
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));
probTickMarks.put(new Double(99.6), new NumberTick(TickType.MINOR, 99.6, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.7), new NumberTick(TickType.MINOR, 99.7, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.8), new NumberTick(TickType.MINOR, 99.8, "",
textAnchor, TextAnchor.CENTER, 0.0));
probTickMarks.put(new Double(99.9), new NumberTick(TickType.MAJOR,  99.9, "99.9",
textAnchor, TextAnchor.CENTER, 0.0));
}
else // on zoom in, use evenly spaced tick marks in percentage space and all major ticks
{
double minRange = range.getLowerBound();
double trialRange = 2.0;

while(true)
{
if(trialRange <= deltaRange)
{
break;
}
else
{
trialRange = trialRange/10.0;
}
}

double tickSize = trialRange/2.0;
int exponent = Math.abs((int) Math.log10(tickSize));
tickSize = setPrecision(tickSize, exponent);

double minTick = setPrecision(minRange, exponent);

// a right click on the chart panel, and selecting Zoom Out can zoom the
// axis limits beyond the lower limit. This prevents a tick mark being
// shown for values < the lowerLimit
if(minTick < lowerLimit) minTick = lowerLimit;

NumberFormat form = NumberFormat.getInstance();
form.setMaximumFractionDigits((exponent));
form.setMinimumFractionDigits((exponent));

for(int i=0; i<21; i++)
{
Double tickVal = new Double(minTick + i*tickSize);
// This next block fixes a corner case
// If a plot is zoomed out, and then zoomed in so that
// the axis length < 10, and the tick size is 1.0, then the
// 0.1 value of the axis will display as a 0, because only one
// digit is displayed when the tickSize is 1, and the tick marks will
// be at 1.1, 2.1, etc, instead of 1.0, 2.0, etc., but will be labeled
// incorrectly as 1.0, 2.0, etc.
if(tickVal <= lowerLimit)
{
tickVal = new Double(lowerLimit);
NumberFormat form1 = NumberFormat.getInstance();
form1.setMaximumFractionDigits(Math.max(1, exponent));
form1.setMinimumFractionDigits(Math.max(1, exponent));
probTickMarks.put(tickVal, new NumberTick(TickType.MAJOR,  tickVal.doubleValue(),
form1.format(tickVal.doubleValue()), textAnchor, TextAnchor.CENTER, 0.0));
minTick = 0.0;  // otherwise can add 1.0 step size to 0.1 minTick and get a 1 label
// at 1.1 position
continue;
}
// a right click on the chart panel, and selecting Zoom Out can zoom the
// axis limits beyond the upper limit. This prevents a tick mark being
// shown for values > the upperLimit, and ensures that 99.9 can be shown
// as a tick mark when the step size is 1.0, and the axis extends beyond 99.9
if(tickVal.doubleValue() >= upperLimit)
{
tickVal = new Double(upperLimit);
NumberFormat form1 = NumberFormat.getInstance();
form1.setMaximumFractionDigits(Math.max(1, exponent));
form1.setMinimumFractionDigits(Math.max(1, exponent));
probTickMarks.put(tickVal, new NumberTick(TickType.MAJOR,  tickVal.doubleValue(),
form1.format(tickVal.doubleValue()), textAnchor, TextAnchor.CENTER, 0.0));
break;
}
probTickMarks.put(tickVal, new NumberTick(TickType.MAJOR,  tickVal.doubleValue(),
form.format(tickVal.doubleValue()), textAnchor, TextAnchor.CENTER, 0.0));
}
}
}

/**
* Adjusts the axis range to match the data range that the axis is
* required to display.
*/
{
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;
}

// This section limits the zoom in to a minimum length of 0.1. This limit cannot be
// extended unless the caclulateCNF and calculateInvCNF methods are replaced
// with more accurate methods. The minimum range length is about the accuracy
// of these methods, for the zoom in to show accurate tick marks.
double length = getRange().getLength();

if(length < minLength)
{
double center = getRange().getCentralValue();
Range range = new Range(center - minLength/2.0, center + minLength/2.0);
if(range.getUpperBound() > upperLimit)
{
range = new Range(upperLimit - minLength, upperLimit);
}
else if(range.getLowerBound() < lowerLimit)
{
range = new Range(lowerLimit, lowerLimit + minLength);
}
setRange(range);
}

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)
{
}
}
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;
}

// This section limits the zoom in to a minimum length of 0.1. This limit cannot be
// extended unless the caclulateCNF and calculateInvCNF methods are replaced
// with more accurate methods. The minimum range length is about the accuracy
// of these methods, for the zoom in to show accurate tick marks.
double length = getRange().getLength();
if(length < minLength)
{
double center = getRange().getCentralValue();
Range range = new Range(center - minLength/2.0, center + minLength/2.0);
if(range.getUpperBound() > upperLimit)
{
range = new Range(upperLimit - minLength, upperLimit);
}
else if(range.getLowerBound() < lowerLimit)
{
range = new Range(lowerLimit, lowerLimit + minLength);
}
setRange(range);
}

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)
{
}
}
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)
{

answer = a[a.length - 1];    // the evaluated polynomial

for(int i = a.length - 2; i >= 0; i--)
{
}
}

* @see java.lang.Object#hashCode()
*/
@Override
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;
}

* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) 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 largest value represented by the axis.
*
* @return The largest value represented by the axis.
*
* @see #setLargestValue(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 < lowerLimit)
{
value = lowerLimit;
}
this.smallestValue = value;
notifyListeners(new AxisChangeEvent(this));
}

/**
* Sets the largest value represented by the axis and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param value  the value.
*
* @see #getLargestValue()
*/
public void setLargestValue(double value) {
if (value > upperLimit)
{
value = upperLimit;
}
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;
if (isInverted()) {
double logA = log1 + length * (1 - upperPercent);
double logB = log1 + length * (1 - lowerPercent);
}
else {
double logA = log1 + length * lowerPercent;
double logB = log1 + length * upperPercent;
}
}

* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return "ProbabilityAxis [smallestValue = " + smallestValue + ", largestValue = " + largestValue + "]";
}

/**<p>
*  This method takes a decimal number, and rounds it so that only the desired number
*  of digits to the right of the decimal point are retained. So an entry of
*  (1.052678, 3) would return 1.053. An entry of (0.6652248, 3) would return
*  0.665. Negative values for the number of decimal places will convert digits to
*  the left of the decimal point to 0, so that an entry of (12345.0, -3) will
*  return 12000.0.</p><p>
*
*  Verified with JUnit test.</p><p>
*
*  <b>CAUTION: </b> This method will only work for inputs that produce an intermediate
*  calculation where the value to be rounded times 10 raised to the number of decimal
*  places to be retained is between the Long min and max values of -2<sup>63</sup>
*  and 2<sup>63</sup> -1 </p>
*
*   <p>author  John St. Ledger, 4/25/14</p>
*
*  @throws  IllegalArgumentException if an intermediate calculation is <= Long.MIN_VALUE,
*                                    or >= Long.MAX_VALUE
*
* @param value          The value to truncate and round the number of decimals.
* @param numberPlaces   The number of decimal places to retain to the right of the decimal.
* @return     The input value rounded and then truncated to the desired precision.
*/
static public double setPrecision(double value, int numberPlaces)
{
double size = Math.pow(10, numberPlaces);
long rnd = Math.round(value*size);
if(rnd == Long.MAX_VALUE) throw new IllegalArgumentException("ProbabilityAxis.setPrecision() arguments are too large");
if(rnd == Long.MIN_VALUE) throw new IllegalArgumentException("ProbabilityAxis.setPrecision() arguments are too small");
return rnd/size;
}

}
``````
John

david.gilbert
Posts: 11707
Joined: Fri Mar 14, 2003 10:29 am
antibot: No, of course not.
Contact:

### Re: New Axis Type ProbabilityAxis

Thanks John. I have been reviewing the code, it looks good to me so far. I am trying to include it for the 1.0.18 release.
David Gilbert Read my blog Ask your company to buy the JFreeChart Developer Guide Check out other products sold by my company Object Refinery Limited

daress
Posts: 23
Joined: Tue Mar 13, 2007 4:52 pm
Contact:

### Re: New Axis Type ProbabilityAxis

John:
Thanks for the ProbabilityAxis - I have downloaded it and been using it for some plot testing. I would suggest however that you change the name to NormalProbabiltyAxis as it is based on the normal distribution. I suggest this as I am thinking of developing a WeibullProbabilityAxis class, unless you have already developed one and are willing to share it.

David G:
Never found this class in either 1.0.18 or 1.0.19. Did you take exception to the class for some reason or did it slip through the bytes of JFreeChart?

stledger
Posts: 14
Joined: Sun Mar 09, 2008 7:39 am

### Re: New Axis Type ProbabilityAxis

daress,

I agree that NormalProbabilityAxis would be a better name. I have not made an axis for any other distributions. I mostly use log-normal distributions in my work. Please feel free to add another axis type.

Nice to see it was useful for someone.

Thanx,

John