package org.freehep.j3d.plot;
import java.io.*;
/**
* @author Joy Kyriakopulos (joyk@fnal.gov)
* @version $Id: AxisLabelCalculator.java 8584 2006-08-10 23:06:37Z duns $
*/
public class AxisLabelCalculator
{
/* VARIABLES */
private double data_min = 0d, data_max = 1d; // actual min/max for the data set
private double plot_min = 0d, plot_max = 1d; // min/max on the axis itself
private AxisLabel[] labels;
private int nDivisions = 0;
private boolean labelsValid;
public void createNewLabels(double min, double max)
{
data_min = min;
data_max = max;
int minNumberOfDivisions = 1;
int maxNumberOfDivisions = 10;
double log10 = Math.log(10.0);
int maxCharsPerLabel = 5;
labelsValid = true;
// log_max is the logarithm of the max value of the data
double log_max = data_max == 0d ? 0d : Math.log(Math.abs(data_max)) / log10;
// int_log_max is the floored integer equivalent of log_max
final int int_log_max = (int) Math.floor(log_max);
// by default, scale_power is 0
// This number represents the amount by which we scale all labels. For example,
// if our range is from 2,000,000,000 to 5,000,000,000 we want scale_power to be
// 9 so that we get 2.0, 2.5, 3.0, ... or something like that on the labels.
int scale_power = 0;
if (int_log_max >= maxCharsPerLabel)
// we have an order of magnitude that can't be displayed in standard form, so we need to set
// a value for scale_power
scale_power = int_log_max;
else if (int_log_max <= -maxCharsPerLabel)
// we have an order of magnitude that can't be displayed in standard form, so we need to set
// a value for scale_power
scale_power = int_log_max;
// System.out.println();
// System.out.println("int_log_max = " + int_log_max + ", maxCharsPerLabel = " + maxCharsPerLabel + ", scale_power = " + scale_power);
final DoubleNumberFormatter format = new DoubleNumberFormatter(scale_power);
// the formatter uses scale_power when creating labels
/*
* Our strategy here is based on the observation that plotting the range
* 0.2 to 40 is very similar to the task of plotting the ranges 2 to 400,
* or 0.02 to 4. We scale the min and max by an order of ten such that when
* converted to integers (by a process not quite like trucation) the difference
* between those two integers is a number greater than or equal to 10 and less
* than (but not equal to) 100. In other words, there will be a difference of
* exactly two digits, which is a sensible precision to see in variation between
* axis labels. For example, the ranges listed above would all yield the integer
* pair 1 to 40 (because the difference between those integers has two digits).
* Given a pair of integers, the function proceeds to calculate appropriate labels.
* If we are using the suggested range, we just round the min down to the next nice
* label and we round the max up to the next nice label (unless the max and min are
* already on nice labels).
*/
final double difference = data_max - data_min;
final double pow = Math.floor(Math.log(difference) / log10) - 1.0;
// pow is the power of 10 used to get integers of the appropriate range
int fractDigits = 0;
if (scale_power > 0)
fractDigits = scale_power - (int) pow;
else if (pow < -0.5)
// we use -0.5 instead of 0.0 in case a Math.floor() returns something that should be 0.0
// but is really just barely under 0.0
fractDigits = scale_power - (int) pow;
final double conversion = Math.pow(10.0, pow);
// this is the actual conversion factor we used, stored once to keep from
// having to recalculate it
int intMin = round(data_min / conversion, false);
int intMax = round(data_max / conversion, true);
// we now have intMin and intMax: the integer pair with a two-digit difference
plot_min = data_min;
plot_max = data_max;
final int naturalNumberOfDivisions = intMax - intMin;
// this number has precisely two digits
int nDivisions = 0;
final float idealMinFraction = 0.5f;
// we will allow as few as this fraction of the maximum number of labels if it is convenient
int nUnits = 1;
// this number can represent two things:
// a) if naturalNumberOfDivisions < maxNumberOfDivisions
// it represents the number of divisions (units) between the natural divisions
// b) if naturalNumberOfDivisions > maxNumberOfDivisions
// it represents the number of units between divisions (the number to skip between divisions)
// 1 is the default value, but we will see if a different value might be better
if (naturalNumberOfDivisions < maxNumberOfDivisions)
// we might like to put in some new divisions
{
final float proximity = (float) naturalNumberOfDivisions / (float) maxNumberOfDivisions;
// this number measures how close the natural number is to the maximum
boolean niceDivisionFound = false;
if (proximity < idealMinFraction)
// we want to do something because the number we have is below the range we want
{
final int[] divisions = {2, 4, 5, 10, 20};
// These are the numbers of subdivisions we might want to place between natural divisions.
// The array only goes up to 20 because we would need a plot with at least 200 labels before
// needing to go any higher.
for (int i = 0; i < divisions.length; i++)
{
final int candidate = divisions[i];
if (proximity * candidate <= 1.0)
// this might work, because the number is small enough that we could fit
// this many divisions on the axis
{
niceDivisionFound = true;
nUnits = candidate;
// the next iteration might be even better, so we...
continue;
}
// if we didn't execute continue, it was because we can't fit this many
// divisions on the axis, and so there's no point in trying the next
// iteration either because it's even bigger, so we break out of the loop
break;
}
}
if (niceDivisionFound)
{
nDivisions = naturalNumberOfDivisions * nUnits + (int) ((plot_max / conversion - intMax) * nUnits);
/*
* The first term in the expression isn't tough: If nUnits is 2 then we need twice as many
* divisions on the axis. The second term isn't so obvious. Suppose our axis goes from 0
* to 12.7 and we decide to set nUnits to 2. We therefore get labels 0.0, 0.5, 1.0, ... , 12.0
* but we won't get 12.5 on there. There will instead be empty space where the 12.5 should go.
* The last term accounts for this, by taking the difference between the top label and the
* axis max (in this case 12.7 - 12.0 = 0.7), multiplying bn nUnits (to get 1.4 in this case)
* and truncating (to get 1 in this case). The result (1 in this case) is the number of labels
* extra we need.
*/
if (pow < 0.5 || scale_power > 0)
// we use 0.5 instead of 1.0 in case a Math.floor() returns something
// that should be 1.0 but is really just barely under 1.0
fractDigits++;
// we've gone down to one lover decimal level so we have to tell the formatter
if ((nUnits == 4 || nUnits == 20) && (pow < 1.5 || scale_power > 0)) // we've actually gone down two decimal levels, so...
// we use 1.5 instead of 2.0 in case a Math.floor() returns something
// that should be 2.0 but is really just barely under 2.0
fractDigits++; // we add another
}
else
// we give up and keep the natural number, even though it's smaller that what we'd like
{
nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);
}
}
else if (naturalNumberOfDivisions > maxNumberOfDivisions)
// the natural number is larger than what we'd like, so we have to skip over some
// (typically this is the more common problem)
{
nDivisions = 1;
// we supply this initialization to make the compiler happy, but in the algorithm
// requires that this initial value change
final int[] skips = {2, 5, 10, 20, 25, 50};
// These are the numbers of natural divisions we're going to try skipping.
for (int i = 0; i < skips.length; i++)
{
final int nDivisionsThisTry = naturalNumberOfDivisions / skips[i];
if (nDivisionsThisTry > maxNumberOfDivisions)
{
// this many skips isn't big enough, so we'll try the next one
continue;
}
// we now assign to nUnits the number of natural divisions to skip over
nUnits = skips[i];
nDivisions = nDivisionsThisTry;
if (nUnits >= 10 && nUnits != 25 && fractDigits > 0)
// we're skipping at least an order of 10, and we're not in quarters, so...
fractDigits--; // we can get rid of one fraction digit
/*
* We may calculate a new value for the intMin. Consider, for example,
* the range 3 to 17. If we decide that our skip will be 2, we will get
* labels like 3, 5, 7, 9, etc. This will look dumb because we would much rather
* have the first label a nice multiple of our skip (i.e., we would rather
* have 2, 4, 6, 8, etc., or for multiples of 5 we would rather have 20,
* 25, 30, 35, etc. over 18, 23, 28, 33, etc.) Therefore, if the intMin is not
* a nice multiple of that skip then we increase the intMin, and we may have
* to decrement nDivisions because of a lost label.
*/
if (intMin % nUnits != 0)
// only true if we are not using the suggested range
{
// increase is the amount we will increase intMin by to make it a nice multiple of nUnits
final int increase = intMin > 0 ? nUnits - intMin % nUnits : -intMin % nUnits;
if (increase > intMax - (intMax - intMin) / nUnits * nUnits - intMin)
// we have put the last label over the limit, so...
nDivisions--; // we drop the last label
intMin += increase;
}
// We are happy with what we've got because it gives us an acceptable
// number of divisions. We don't want to go any higher because that
// will just make for fewer divisions, so we...
break;
}
}
else // hey! they're exactly equal
nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);
double minLabelValue = intMin * conversion;
final double inc = naturalNumberOfDivisions < maxNumberOfDivisions ? conversion / nUnits : conversion * nUnits;
if (naturalNumberOfDivisions < maxNumberOfDivisions && minLabelValue - inc >= plot_min)
// this happens if we are dividing up divisions, and we get divisions below intMin * conversion
{
int nLost = (int) ((minLabelValue - inc) / inc);
minLabelValue -= nLost * inc;
nDivisions += nLost;
}
labels = new AxisLabel[nDivisions + 1];
this.nDivisions = nDivisions;
// System.out.println("fractDigits = " + fractDigits + ", minLabelValue = " + minLabelValue + ", inc = " + inc);
format.setFractionDigits(fractDigits);
for (int j = 0; j < labels.length; j++)
{
final double labelValue = minLabelValue + j * inc;
labels[j] = new AxisLabel();
labels[j].text = format.format(labelValue);
labels[j].position = (labelValue - plot_min) / (plot_max - plot_min);
// labels[j].logPosition = Math.log(labels[j].position + Math.E);
if (labels[j].position < 0.) labels[j].position = 0.;
}
}
private int charsReq(int pow)
/*
* Returns the number of characters required (not using scientific
* notation) to represent a decade of the given order of magnitude.
*/
{
if (pow < 0)
/*
* If the power is less than 0, we need one space for the
* leading zero, one for the decimal, and then -pow spaces
* for each of the following characters. For example, if
* pow = -2 then the result would be 4, because there are
* four characters in the string "0.01".
*/
return -pow + 2;
else
/*
* If the power is 0 or greater, we need one character for
* the character '1' and one '0' for each order of magnitude.
*/
return pow + 1;
}
private int round(final double d, final boolean down)
/*
* Determines an integer value from a double by rounding intelligently.
* In the logarithmic case, when determining the order of magnitude
* of the lowest tick, the "down" parameter is false because
* we want to round up from the minimum point in the axis (so that
* the tick shows up on the range of the axis) if we are not very close
* to an integer value. However, when we are determining the order
* of magnitude of the highest tick, we want to round down if we are
* not very close to an integer so that the tick appears within the
* range of the axis. Similarly in the case for the linear axis, we round
* up to get the smallest tick value and we round down to get the largest
* tick value. We do exactly the opposite if wew are using the suggested
* range. In that case, we round down to get the minimum and up to get
* the maximum.
*/
{
final double minProximity = 0.0001;
/*
* A parameter's proximity to the nearest integer must be this
* fraction of its size in order to be considered that value.
*/
final double round = Math.round(d); // the closest integer value
if (d == round || Math.abs(d - round) < (d != 0.0 ? minProximity * Math.abs(d) : 0.000001))
{
// we assume here that d is close enough to be an integer, so we round and return
return (int) round;
}
else
{
// d is not close enough to be an integer, so we return the
// floor if we were supposed to round down and ceil
// otherwise
return down ? (int) Math.floor(d) : (int) Math.ceil(d);
}
}
public String [] getLabels()
{
int len = labels.length;
String [] lab = new String [len];
for (int i = 0; i < len; ++i)
lab[i] = labels[i].text;
return lab;
}
public double [] getPositions()
{
int len = labels.length;
double [] pos = new double [len];
for (int i = 0; i < len; ++i)
pos[i] = labels[i].position;
return pos;
}
public void printLabels()
{
System.out.println("data_min = " + data_min + ", data_max = " + data_max);
System.out.println("plot_min = " + plot_min + ", plot_max = " + plot_max);
System.out.println("nDivisions = " + nDivisions + ", labelsValid = " + labelsValid);
int i;
for (i = 0; i < labels.length; ++i)
{
System.out.println("label "+ (i+1) + ": " + labels[i].text + " position: " + labels[i].position);
}
}
// This class is used when storing information about
// the labels on the axis.
private class AxisLabel
{
String text;
double position;
// double logPosition;
}
/**
* Just for testing
*/
public static void main(String[] argv)
{
AxisLabelCalculator t = new AxisLabelCalculator();
t.createNewLabels(0., 1.);
t.printLabels();
t.createNewLabels(0., 100.);
t.printLabels();
t.createNewLabels(0.0001, 0.001);
t.printLabels();
t.createNewLabels(5., 10.);
t.printLabels();
t.createNewLabels(.001, .050);
t.printLabels();
t.createNewLabels(.00001, .00005);
t.printLabels();
t.createNewLabels(-.001, .00005);
t.printLabels();
t.createNewLabels(-.001, .005);
t.printLabels();
t.createNewLabels(-2., 5.);
t.printLabels();
t.createNewLabels(.05, 10.);
t.printLabels();
}
}